Testing precompiled script plugins with version catalogs (LibrariesForLibs)

I use @Vampire 's hack to access libs in plugins. It works in actual build, but fails in unit/functional tests.

build-src/build.gradle.kts
dependencies {
    implementation(files(libs.javaClass.superclass.protectionDomain.codeSource.location))
}
build-src/src/main/kotlin/my/Libs.kt
// Is this okay if I want to reuse?
internal val Project.libs get() = the<LibrariesForLibs>()
build-src/src/main/kotlin/my/convention.gradle.kts
plugins {
    android
}

dependencies {
    implementation(libs.someLib)
}
build-src/src/test/kotlin/my/ConventionTest.kt
class ConventionTest {
    @Test fun test() {
        val project = ProjectBuilder.builder().build().also {
            it.plugins.apply("my.convention")
        }
        // ...
    }
}
build-src/src/test/kotlin/my/ConventionFunctionalTest.kt
class ConventionFunctionalTest {

    @field:TempDir
    lateinit var projectDir: File

    private val buildFile by lazy { projectDir.resolve("build.gradle.kts") }
    private val libsVersions by lazy { projectDir.resolve("gradle/libs.versions.toml") }

    @Test
    fun test() {
        buildFile.writeText(
            """
            plugins {
                id("my.convention")
            }
            """.trimIndent()
        )
        libsVersions.apply { parentFile.mkdir() }.writeText(
            """
            [versions]
            someLib = "1.0.0"

            [libraries]
            someLib = { module = "org.example:some-lib", version.ref = "someLib" }
            """.trimIndent()
        )
        val result = GradleRunner.create()
            .withPluginClasspath()
            .withArguments("-q", "help")
            .withProjectDir(projectDir)
            .build()
        // ...
    }
}

This is the error message of ConventionTest:

Caused by: org.gradle.api.UnknownDomainObjectException: Extension of type 'LibrariesForLibs' does 
not exist. Currently registered extension types: [ExtraPropertiesExtension, BasePluginExtension, 
DefaultArtifactPublicationSet, SourceSetContainer, ReportingExtension, JavaToolchainService, 
JavaPluginExtension, BaseAppModuleExtension, ApplicationAndroidComponentsExtension, 
NamedDomainObjectContainer<BaseVariantOutput>]

This is the error message of ConventionFunctionalTest:

* What went wrong:
An exception occurred applying plugin request [id: 'my.convention']
> Failed to apply plugin 'my.convention'.
   > Extension of type 'LibrariesForLibs' does not exist. Currently registered extension types: 
[ExtraPropertiesExtension, LibrariesForLibs, VersionCatalogsExtension, BasePluginExtension, 
DefaultArtifactPublicationSet, SourceSetContainer, ReportingExtension, JavaToolchainService, 
JavaPluginExtension, BaseAppModuleExtension, ApplicationAndroidComponentsExtension, 
NamedDomainObjectContainer<BaseVariantOutput>]

Is there another workaround for this?

Thank you,

I have no idea I never tried this with tests, it might well be that it does not work.

For ConventionTest it might work if you add a mocked extension of that type before you apply the plugin.

For ConventionFunctionalTest it says LibrariesForLibs is not found but LibrariesForLibs is found, so those are probably two different classes from two different classloaders. I have no idea whether you can somehow make this work, you probably have to fiddle around and debug around to find out whether this can be made working somehow. :man_shrugging:

For ConventionFunctionalTest, I printed extensions.findByType<LibrariesForLibs>() in both the plugin and the test build which applied the plugin. In the plugin it printed null but in the test build script it printed extension 'libs'. I also printed project.name and System.identityHashCode(project), both of them printed the same values. Could this be because the test kit injects the extension after applying the plugin?

For ConventionTest it might work if you add a mocked extension of that type before you apply the plugin.

Thank you, it works. Correct me if the code is wrong.

        val libs = mockk<LibrariesForLibs> {
            every { versions.someLib.get() } returns "1.0.0"
            // more mocks ...
        }
        val project = ProjectBuilder.builder().build().also {
            it.extensions.add("libs", libs)
            it.plugins.apply("my.convention")
        }
        // the test and verification ...

Maybe, I don’t use mockk or kotest, I use Spock for testing.

And I never had tests with that hack-around.
For “local” convention plugins I most often do not write tests, for published plugins I don’t use the hack-around.