Repository: mockito/mockito
Issue: MockitoExtension cannot be used with JUnit 5 in OSGi integration tests

The Mockito junit-jupiter JAR is missing OSGi bundle manifest entries so it cannot be used in OSGi integration tests.

We run some OSGi integration tests (using bnd) that have `@Mock`s so it would be easier if these mocks can be initialized by annotating classes with `@MockitoExtension` instead of using `MockitoAnnotations.openMocks(this)`, see for example this [FirmwareUpdateServiceTest](https://github.com/openhab/openhab-core/blob/43cb68c9d753faf3a88cfd19c4fc015c8289f880/itests/org.openhab.core.thing.tests/src/main/java/org/openhab/core/thing/internal/firmware/FirmwareUpdateServiceTest.java).

What may complicate adding the manifest entries is that `MockitoExtension` is [importing](https://github.com/mockito/mockito/blob/b87fad310bf59b94035028dc478089ded2a9198a/subprojects/junit-jupiter/src/main/java/org/mockito/junit/jupiter/MockitoExtension.java#L27-L29) internal classes which [aren't exported](https://github.com/mockito/mockito/blob/b87fad310bf59b94035028dc478089ded2a9198a/gradle/mockito-core/osgi.gradle#L19) by the mockito-core bundle. What may help is to make junit-jupiter a bundle fragment with mockito-core as host.

Issue comments:
Fragments can be a bit of a pain.

@rotty3000 could offer some advice here - he did the OSGi metadata port for the JUnit 5 project itself. They had a similar problem, in that some of the bundles relied on internal packages of other bundles. I think that they ended up exporting the "internal" packages, but flagged them with an attribute to mark them as "internal use only".
I would also like to see a solution to this issue. Making the mockito-junit-jupiter jar into a fragment which exports the `MockitoExtension` package and attaches to mockito-core does sound like a good plan.
I've got a set of changes that makes mockito-junit-jupiter work in OSGi using the same strategy that junit5 used to identify "friend" packages that are exported but not intended for public use outside of _mockito_.

@raphw do you think your team would be interested?
Looks good to me, I had a look at the build and I know too little about OSGi to understand the effect the API guardian dependencies have but since the classes only carry annotations and the dependency is not required, I do not see any chance of a regression.

@TimvdLippe - could you have a second look, you know the build better then me. To me it looks like a good merge.
For your edification:
a) apiguardian does not bring with it any runtime dependencies (confirmed by the OSGi metadata)
b) the interaction between [API guardian and bnd is explained here](https://bnd.bndtools.org/instructions/export-apiguardian.html)
c) the build adds a `verifyOSGi` gradle task glued onto `check` to make sure that future changes do not break the relationship required between mockito-core and mockito-junit-jupiter such that they continue working together in OSGi
Today I tried a recent version of Mockito 5 with OSGi but it doesn't work anymore, see also #2708. :disappointed: 
Perhaps some of the commenters here can help with fixing it?
@wborn Unfortunately I don't have a lot of experience with OSGi, but would be happy to review a PR that fixes it (with an included regression test). Apologies for the breakage!
I think there needs to be some form of import of the boot loader package that we add. Not sure if such an import is considered in OSGi, but I'd need a reproduction to look into it.

Pull request review comments:
It's not clear to me why this part was removed and what changed it. Could you please add a comment to the functionality that replaces it to explain why we need it? (E.g. reproducible jars) And could you update your commit message to explain the change that was made.
Could you add an example in the comment what would be invalid?
bnd has this feature builtin! I enabled it by adding `-noextraheaders: true` rendering this code obsolete.
if you diff the results before and after (which I did to double check) you will see what I mean :)
An error will cause the build to fail with something like this:
```
> Task :junit-jupiter:verifyOSGi FAILED
Resolution failed. Capabilities satisfying the following requirements could not be found:
    [<<INITIAL>>]
      ⇒ osgi.identity: (osgi.identity=org.mockito.junit-jupiter)
          ⇒ [org.mockito.junit-jupiter version=3.5.11]
              ⇒ osgi.wiring.package: (osgi.wiring.package=com.foo)
```
I triggered this failure by manually importing a fake package `com.foo` (to replicate some package which cannot be satisfied by the available runtime dependencies) which is the same effect for any change that would inadvertently break the OSGi support.
If such a failure ever happens, you can ping me and I would be more than happy to help track down the reason (though I think it might become obvious to you pretty quickly :) )

Patch that resolved the issue:
diff --git a/gradle/mockito-core/osgi.gradle b/gradle/mockito-core/osgi.gradle
index 2fa0b78354..0c6f395e8b 100644
--- a/gradle/mockito-core/osgi.gradle
+++ b/gradle/mockito-core/osgi.gradle
@@ -1,12 +1,3 @@
-import java.nio.file.attribute.FileTime
-import java.util.jar.Attributes
-import java.util.jar.JarEntry
-import java.util.jar.JarFile
-import java.util.jar.JarOutputStream
-import java.util.jar.Manifest
-
-import static java.util.Calendar.FEBRUARY
-
 apply plugin: 'biz.aQute.bnd.builder'
 
 jar {
@@ -14,9 +5,9 @@ jar {
     bnd(
         'Bundle-Name': 'Mockito Mock Library for Java. Core bundle requires Byte Buddy and Objenesis.',
         'Bundle-SymbolicName': 'org.mockito.mockito-core',
-        'Bundle-Version': project.version.replace('-', '.'),
+        'Bundle-Version': "\${version_cleanup;${project.version}}",
         '-versionpolicy': '[${version;==;${@}},${version;+;${@}})',
-        'Export-Package': "!org.mockito.internal.*,org.mockito.*;version=${version}",
+        'Export-Package': "org.mockito.internal.*;status=INTERNAL;mandatory:=status;version=${version},org.mockito.*;version=${version}",
         'Import-Package': [
                 'net.bytebuddy.*;version="[1.6.0,2.0)"',
                 'junit.*;resolution:=optional',
@@ -27,89 +18,7 @@ jar {
                 'org.mockito.*'
         ].join(','),
         '-removeheaders': 'Private-Package',
-        'Automatic-Module-Name': 'org.mockito'
+        'Automatic-Module-Name': 'org.mockito',
+        '-noextraheaders': 'true'
     )
 }
-
-jar.doLast {
-    JarFile originalJar = new JarFile(jar.archivePath)
-
-    new RemoveOsgiLastModifiedHeader(originalJar)
-        .transform()
-        .renameTo jar.archivePath
-}
-
-
-/*
- * The OSGi plugin in Gradle 5.3 has a known issue (https://github.com/gradle/gradle/issues/6187) where it embeds
- * a Manifest entry "Bnd-LastModified" (set to the last modified time of the build directory) even if you ask it not to
- * using `-noextraheaders` or `-removeheaders`.
- *
- * It cannot be pinned or removed, and is a source of non-determinism in the build.
- *
- * This class addresses the problem by rewriting the JAR and removing that entry from the Manifest. A side-effect of this
- * is having to set `lastModifiedTime` for each entry to a fixed value, else this would also introduce non-determinism.
- *
- * The fixed value is the same as the default value for Gradle's fixed timestamps, and the rationale behind it is detailed
- * in https://github.com/gradle/gradle/blob/58538513a3bff3b7015718976fe1304e23f40694/subprojects/core/src/main/java/org/gradle/api/internal/file/archive/ZipCopyAction.java#L42-L57
- */
-
-class RemoveOsgiLastModifiedHeader {
-    private final long CONSTANT_TIME_FOR_ZIP_ENTRIES = new GregorianCalendar(1980, FEBRUARY, 1, 0, 0, 0).timeInMillis
-
-    private final JarFile artifact
-
-    RemoveOsgiLastModifiedHeader(JarFile artifact) {
-        this.artifact = artifact
-    }
-
-    File transform() {
-        File temp = File.createTempFile("temp",".jar")
-
-        temp.withOutputStream { os ->
-            JarOutputStream stream = new JarOutputStream(os)
-
-            this.artifact
-                .entries()
-                // Keep META-INF folder at the beginning to avoid https://github.com/mockito/mockito/issues/2108
-                .sort { (it.name.startsWith("META-INF") ? "A/" + it.name : "B/" + it.name) }
-                .each { JarEntry entry ->
-
-                    stream.putNextEntry(newJarEntry(entry))
-
-                    if (entry.name == "META-INF/MANIFEST.MF") {
-                        newManifest(entry).write(stream)
-                    } else {
-                        stream << this.artifact.getInputStream(entry)
-                    }
-
-                    stream.closeEntry()
-                }
-
-            stream.finish()
-        }
-
-        temp
-    }
-
-    Manifest newManifest(JarEntry entry) {
-        Manifest manifest = new Manifest()
-
-        manifest.read(this.artifact.getInputStream(entry))
-
-        manifest
-            .getMainAttributes()
-            .remove(new Attributes.Name("Bnd-LastModified"))
-
-        manifest
-    }
-
-    JarEntry newJarEntry(JarEntry original) {
-        JarEntry newEntry = new JarEntry(original.name)
-
-        newEntry.setLastModifiedTime(FileTime.fromMillis(CONSTANT_TIME_FOR_ZIP_ENTRIES))
-
-        newEntry
-    }
-
-}
diff --git a/subprojects/junit-jupiter/junit-jupiter.gradle b/subprojects/junit-jupiter/junit-jupiter.gradle
index 7cfc0b7de1..6e24b6a861 100644
--- a/subprojects/junit-jupiter/junit-jupiter.gradle
+++ b/subprojects/junit-jupiter/junit-jupiter.gradle
@@ -1,6 +1,11 @@
+import aQute.bnd.gradle.BundleTaskConvention
+import aQute.bnd.gradle.FileSetRepositoryConvention
+import aQute.bnd.gradle.Resolve
+
 description = "Mockito JUnit 5 support"
 
 apply from: "$rootDir/gradle/java-library.gradle"
+apply plugin: 'biz.aQute.bnd.builder'
 
 dependencies {
     compile project.rootProject
@@ -13,3 +18,43 @@ dependencies {
 tasks.withType(Test) {
     useJUnitPlatform()
 }
+
+jar {
+    classpath = project.configurations.runtimeClasspath
+    bnd(
+        'Bundle-Name': 'Mockito Extension Library for JUnit 5.',
+        'Bundle-SymbolicName': 'org.mockito.junit-jupiter',
+        'Bundle-Version': "\${version_cleanup;${project.version}}",
+        '-versionpolicy': '[${version;==;${@}},${version;+;${@}})',
+        'Export-Package': "org.mockito.junit.jupiter.*;version=${version}",
+        '-removeheaders': 'Private-Package',
+        'Automatic-Module-Name': 'org.mockito.junit.jupiter',
+        '-noextraheaders': 'true',
+        '-export-apiguardian': 'org.mockito.internal.*'
+    )
+}
+
+def osgiPropertiesFile = file("$buildDir/verifyOSGiProperties.bndrun")
+
+// Bnd's Resolve task uses a properties file for its configuration. This
+// task writes out the properties necessary for it to verify the OSGi
+// metadata.
+tasks.register('osgiProperties', WriteProperties) {
+  outputFile = osgiPropertiesFile
+  property('-standalone', true)
+  property('-runee', "JavaSE-${targetCompatibility}")
+  property('-runrequires', 'osgi.identity;filter:="(osgi.identity=org.mockito.junit-jupiter)"')
+}
+
+// Bnd's Resolve task is what verifies that a jar can be used in OSGi and
+// that its metadata is valid. If the metadata is invalid this task will
+// fail.
+tasks.register('verifyOSGi', Resolve) {
+  dependsOn(osgiProperties)
+  setBndrun(osgiPropertiesFile)
+  reportOptional = false
+}
+
+tasks.check {
+  dependsOn(verifyOSGi)
+}


Test patch: