Skip to content

Commit c81be5d

Browse files
authored
Add support for including module-info in Mockito. (#3597)
Both `mockito-core` and `mockito-junit-jupiter` now ship a `module-info` declaration. All internal packages are not exported.
1 parent d01ac9d commit c81be5d

File tree

14 files changed

+221
-41
lines changed

14 files changed

+221
-41
lines changed

buildSrc/src/main/kotlin/mockito.javadoc-conventions.gradle.kts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,10 @@ tasks.named<Javadoc>("javadoc") {
1515
inputs.dir(javadocConfigDir)
1616
description = "Creates javadoc html for ${project.name}."
1717

18+
// Work-around as suggested in https://github.com/gradle/gradle/issues/19726
19+
val sourceSetDirectories = sourceSets.main.get().java.sourceDirectories.joinToString(":")
20+
val coreOptions = options as CoreJavadocOptions
21+
coreOptions.addStringOption("-source-path", sourceSetDirectories)
1822
exclude("**/internal/**")
1923

2024
// For more details on the format

mockito-core/build.gradle.kts

Lines changed: 39 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,9 @@
11
import com.android.build.gradle.internal.tasks.factory.dependsOn
2+
import org.objectweb.asm.ClassReader
3+
import org.objectweb.asm.ClassVisitor
4+
import org.objectweb.asm.ClassWriter
5+
import org.objectweb.asm.ModuleVisitor
6+
import org.objectweb.asm.Opcodes
27

38
plugins {
49
id("mockito.library-conventions")
@@ -70,11 +75,40 @@ tasks {
7075

7176
from(sourceSets.main.flatMap { it.java.classesDirectory }
7277
.map { it.file("org/mockito/internal/creation/bytebuddy/inject/MockMethodDispatcher.class") })
73-
into(generatedInlineResource.map { it.dir("org/mockito/internal/creation/bytebuddy/inject") })
78+
into(generatedInlineResource.map { it.dir("org/mockito/internal/creation/bytebuddy") })
7479

75-
rename("(.+)\\.class", "$1.raw")
80+
rename(".*", "inject-MockMethodDispatcher.raw")
7681
}
82+
83+
val removeInjectionPackageFromModuleInfo by registering(DefaultTask::class) {
84+
dependsOn(compileJava)
85+
86+
doLast {
87+
val moduleInfo = sourceSets.main.get().output.classesDirs.first().resolve("module-info.class")
88+
89+
val reader = ClassReader(moduleInfo.readBytes())
90+
val writer = ClassWriter(reader, 0)
91+
reader.accept(object : ClassVisitor(Opcodes.ASM9, writer) {
92+
override fun visitModule(name: String?, access: Int, version: String?): ModuleVisitor {
93+
return object : ModuleVisitor(
94+
Opcodes.ASM9,
95+
super.visitModule(name, access, version)
96+
) {
97+
override fun visitPackage(packaze: String) {
98+
if (packaze != "org/mockito/internal/creation/bytebuddy/inject") {
99+
super.visitPackage(packaze)
100+
}
101+
}
102+
}
103+
}
104+
}, 0)
105+
106+
moduleInfo.writeBytes(writer.toByteArray())
107+
}
108+
}
109+
77110
classes.dependsOn(copyMockMethodDispatcher)
111+
classes.dependsOn(removeInjectionPackageFromModuleInfo)
78112

79113

80114
jar {
@@ -112,8 +146,8 @@ tasks {
112146
# Export rules for public and internal packages
113147
# https://bnd.bndtools.org/heads/export_package.html
114148
Export-Package: \
115-
org.mockito.internal.*;status=INTERNAL;mandatory:=status;version=${archiveVersion.get()}, \
116-
org.mockito.*;version=${archiveVersion.get()}
149+
!org.mockito.internal.creation.bytebuddy.inject,org.mockito.internal.*;status=INTERNAL;mandatory:=status;version=${archiveVersion.get()}, \
150+
!org.mockito.internal.creation.bytebuddy.inject,org.mockito.*;version=${archiveVersion.get()}
117151
118152
# General rules for package import
119153
# https://bnd.bndtools.org/heads/import_package.html
@@ -132,15 +166,12 @@ tasks {
132166
org.hamcrest;resolution:=optional, \
133167
org.objenesis;version="[3.1,4.0)", \
134168
org.opentest4j.*;resolution:=optional, \
135-
org.mockito.*
169+
!org.mockito.internal.creation.bytebuddy.inject,org.mockito.*
136170
137171
# Don't add the Private-Package header.
138172
# See https://bnd.bndtools.org/instructions/removeheaders.html
139173
-removeheaders: Private-Package
140174
141-
# Configures the automatic module name for Java 9+.
142-
Automatic-Module-Name: org.mockito
143-
144175
# Don't add all the extra headers bnd normally adds.
145176
# See https://bnd.bndtools.org/instructions/noextraheaders.html
146177
-noextraheaders: true
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
/*
2+
* Copyright (c) 2025 Mockito contributors
3+
* This program is made available under the terms of the MIT License.
4+
*/
5+
module org.mockito {
6+
requires java.instrument;
7+
requires jdk.attach;
8+
requires net.bytebuddy;
9+
requires net.bytebuddy.agent;
10+
requires static junit;
11+
requires static org.hamcrest;
12+
requires static org.opentest4j;
13+
requires static jdk.unsupported;
14+
15+
exports org.mockito;
16+
exports org.mockito.configuration;
17+
exports org.mockito.creation.instance;
18+
exports org.mockito.exceptions.base;
19+
exports org.mockito.exceptions.misusing;
20+
exports org.mockito.exceptions.verification;
21+
exports org.mockito.exceptions.verification.junit;
22+
exports org.mockito.exceptions.verification.opentest4j;
23+
exports org.mockito.hamcrest;
24+
exports org.mockito.invocation;
25+
exports org.mockito.junit;
26+
exports org.mockito.listeners;
27+
exports org.mockito.mock;
28+
exports org.mockito.plugins;
29+
exports org.mockito.quality;
30+
exports org.mockito.session;
31+
exports org.mockito.stubbing;
32+
exports org.mockito.verification;
33+
exports org.mockito.internal.configuration to
34+
org.mockito.junit.jupiter;
35+
exports org.mockito.internal.session to
36+
org.mockito.junit.jupiter;
37+
exports org.mockito.internal.configuration.plugins to
38+
org.mockito.junit.jupiter;
39+
exports org.mockito.internal.util to
40+
org.mockito.junit.jupiter;
41+
}

mockito-core/src/main/java/org/mockito/internal/configuration/plugins/PluginInitializer.java

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
package org.mockito.internal.configuration.plugins;
66

77
import java.io.IOException;
8+
import java.lang.reflect.Method;
89
import java.net.URL;
910
import java.util.ArrayList;
1011
import java.util.Enumeration;
@@ -48,6 +49,7 @@ public <T> T loadImpl(Class<T> service) {
4849
classOrAlias = DefaultMockitoPlugins.getDefaultPluginClass(classOrAlias);
4950
}
5051
Class<?> pluginClass = loader.loadClass(classOrAlias);
52+
addReads(pluginClass);
5153
Object plugin = pluginClass.getDeclaredConstructor().newInstance();
5254
return service.cast(plugin);
5355
}
@@ -80,6 +82,7 @@ public <T> List<T> loadImpls(Class<T> service) {
8082
classOrAlias = DefaultMockitoPlugins.getDefaultPluginClass(classOrAlias);
8183
}
8284
Class<?> pluginClass = loader.loadClass(classOrAlias);
85+
addReads(pluginClass);
8386
Object plugin = pluginClass.getDeclaredConstructor().newInstance();
8487
impls.add(service.cast(plugin));
8588
}
@@ -89,4 +92,17 @@ public <T> List<T> loadImpls(Class<T> service) {
8992
"Failed to load " + service + " implementation declared in " + resources, e);
9093
}
9194
}
95+
96+
private static void addReads(Class<?> pluginClass) {
97+
try {
98+
Method getModule = Class.class.getMethod("getModule");
99+
Method addReads =
100+
getModule.getReturnType().getMethod("addReads", getModule.getReturnType());
101+
addReads.invoke(
102+
getModule.invoke(PluginInitializer.class), getModule.invoke(pluginClass));
103+
} catch (NoSuchMethodException ignored) {
104+
} catch (Exception e) {
105+
throw new IllegalStateException(e);
106+
}
107+
}
92108
}

mockito-core/src/main/java/org/mockito/internal/creation/bytebuddy/InlineDelegateByteBuddyMockMaker.java

Lines changed: 34 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -138,25 +138,29 @@ class InlineDelegateByteBuddyMockMaker
138138
boot.deleteOnExit();
139139
try (JarOutputStream outputStream =
140140
new JarOutputStream(new FileOutputStream(boot))) {
141-
String source =
142-
"org/mockito/internal/creation/bytebuddy/inject/MockMethodDispatcher";
143141
InputStream inputStream =
144-
InlineDelegateByteBuddyMockMaker.class
145-
.getClassLoader()
146-
.getResourceAsStream(source + ".raw");
142+
InlineDelegateByteBuddyMockMaker.class.getResourceAsStream(
143+
"inject-MockMethodDispatcher.raw");
147144
if (inputStream == null) {
148145
throw new IllegalStateException(
149146
join(
150147
"The MockMethodDispatcher class file is not locatable: "
151-
+ source
152-
+ ".raw",
148+
+ "inject-MockMethodDispatcher.raw"
149+
+ " in context of "
150+
+ InlineDelegateByteBuddyMockMaker.class.getName(),
153151
"",
154152
"The class loader responsible for looking up the resource: "
155153
+ InlineDelegateByteBuddyMockMaker.class
156-
.getClassLoader()));
154+
.getClassLoader(),
155+
"",
156+
"The module responsible for looking up the resource: "
157+
+ InlineDelegateByteBuddyMockMaker.class
158+
.getModule()));
157159
}
158160
try (inputStream) {
159-
outputStream.putNextEntry(new JarEntry(source + ".class"));
161+
outputStream.putNextEntry(
162+
new JarEntry(
163+
"org/mockito/internal/creation/bytebuddy/inject/MockMethodDispatcher.class"));
160164
int length;
161165
byte[] buffer = new byte[1024];
162166
while ((length = inputStream.read(buffer)) != -1) {
@@ -168,11 +172,13 @@ class InlineDelegateByteBuddyMockMaker
168172
try (JarFile jarfile = new JarFile(boot)) {
169173
instrumentation.appendToBootstrapClassLoaderSearch(jarfile);
170174
}
175+
Class<?> dispatcher;
171176
try {
172-
Class.forName(
173-
"org.mockito.internal.creation.bytebuddy.inject.MockMethodDispatcher",
174-
false,
175-
null);
177+
dispatcher =
178+
Class.forName(
179+
"org.mockito.internal.creation.bytebuddy.inject.MockMethodDispatcher",
180+
false,
181+
null);
176182
} catch (ClassNotFoundException cnfe) {
177183
throw new IllegalStateException(
178184
join(
@@ -181,6 +187,21 @@ class InlineDelegateByteBuddyMockMaker
181187
"It seems like your current VM does not support the instrumentation API correctly."),
182188
cnfe);
183189
}
190+
try {
191+
InlineDelegateByteBuddyMockMaker.class
192+
.getModule()
193+
.addReads(dispatcher.getModule());
194+
} catch (Exception e) {
195+
throw new IllegalStateException(
196+
join(
197+
"Mockito failed to adjust the module graph to read the dispatcher module",
198+
"",
199+
"Dispatcher: "
200+
+ dispatcher
201+
+ " is loaded by "
202+
+ dispatcher.getClassLoader()),
203+
e);
204+
}
184205
} catch (IOException ioe) {
185206
throw new IllegalStateException(
186207
join(

mockito-core/src/main/java/org/mockito/internal/creation/bytebuddy/inject/package-info.java

Lines changed: 0 additions & 11 deletions
This file was deleted.

mockito-core/src/main/java/org/mockito/internal/util/reflection/InstrumentationMemberAccessor.java

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@
55
package org.mockito.internal.util.reflection;
66

77
import net.bytebuddy.ByteBuddy;
8-
import net.bytebuddy.dynamic.loading.ClassLoadingStrategy;
8+
import net.bytebuddy.dynamic.loading.ByteArrayClassLoader;
9+
import net.bytebuddy.dynamic.loading.InjectionClassLoader;
910
import net.bytebuddy.implementation.MethodCall;
1011
import org.mockito.exceptions.base.MockitoInitializationException;
1112
import org.mockito.internal.PremainAttachAccess;
@@ -52,6 +53,20 @@ class InstrumentationMemberAccessor implements MemberAccessor {
5253
// This way, we assure that classes within Mockito's module (which might be a shared,
5354
// unnamed module) do not face escalated privileges where tests might pass that would
5455
// otherwise fail without Mockito's opening.
56+
InjectionClassLoader classLoader =
57+
new ByteArrayClassLoader(
58+
InstrumentationMemberAccessor.class.getClassLoader(),
59+
false,
60+
Collections.emptyMap());
61+
instrumentation.redefineModule(
62+
Dispatcher.class.getModule(),
63+
Collections.emptySet(),
64+
Collections.singletonMap(
65+
Dispatcher.class.getPackageName(),
66+
Collections.singleton(classLoader.getUnnamedModule())),
67+
Collections.emptyMap(),
68+
Collections.emptySet(),
69+
Collections.emptyMap());
5570
dispatcher =
5671
new ByteBuddy()
5772
.subclass(Dispatcher.class)
@@ -78,12 +93,11 @@ class InstrumentationMemberAccessor implements MemberAccessor {
7893
.onArgument(0)
7994
.withArgument(1))
8095
.make()
81-
.load(
82-
InstrumentationMemberAccessor.class.getClassLoader(),
83-
ClassLoadingStrategy.Default.WRAPPER)
96+
.load(classLoader, InjectionClassLoader.Strategy.INSTANCE)
8497
.getLoaded()
8598
.getConstructor()
8699
.newInstance();
100+
classLoader.seal();
87101
throwable = null;
88102
} catch (Throwable t) {
89103
instrumentation = null;

mockito-core/src/test/java/org/mockito/internal/creation/bytebuddy/InlineDelegateByteBuddyMockMakerTest.java

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -528,13 +528,20 @@ protected static <T> MockCreationSettings<T> settingsFor(
528528
}
529529

530530
@Test
531-
public void testMockDispatcherIsRelocated() throws Exception {
531+
public void testMockDispatcherIsRelocated() {
532532
assertThat(
533533
InlineByteBuddyMockMaker.class
534534
.getClassLoader()
535535
.getResource(
536-
"org/mockito/internal/creation/bytebuddy/inject/MockMethodDispatcher.raw"))
536+
"org/mockito/internal/creation/bytebuddy/inject-MockMethodDispatcher.raw"))
537537
.isNotNull();
538+
539+
assertThat(
540+
InlineByteBuddyMockMaker.class
541+
.getClassLoader()
542+
.getResource(
543+
"org/mockito/internal/creation/bytebuddy/inject/MockMethodDispatcher.class"))
544+
.isNull();
538545
}
539546

540547
private static final class FinalClass {

mockito-extensions/mockito-junit-jupiter/build.gradle.kts

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -53,9 +53,6 @@ tasks {
5353
# See https://bnd.bndtools.org/instructions/removeheaders.html
5454
-removeheaders: Private-Package
5555
56-
# Configures the automatic module name for Java 9+.
57-
Automatic-Module-Name: org.mockito.junit.jupiter
58-
5956
# Don't add all the extra headers bnd normally adds.
6057
# See https://bnd.bndtools.org/instructions/noextraheaders.html
6158
-noextraheaders: true
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
/*
2+
* Copyright (c) 2025 Mockito contributors
3+
* This program is made available under the terms of the MIT License.
4+
*/
5+
module org.mockito.junit.jupiter {
6+
requires transitive org.mockito;
7+
requires transitive org.junit.jupiter.api;
8+
9+
exports org.mockito.junit.jupiter;
10+
exports org.mockito.junit.jupiter.resolver;
11+
}

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy