gradlex-org/extra-java-module-info

Plugin forces all dependencies onto the modulepath when running whitebox tests.

Closed this issue · 5 comments

Hi. Looks like a great plugin!

There is one issue I am having with it: The plugin appears to be forcing all dependencies onto the modulepath, when running whitebox tests (without a module-info in the src/test/java directory).

For example, I have a project with a legacy library declared as testImplementation("com.tngtech.archunit:archunit-junit5:0.14.1") and no module-info.java in the src/test/java directory.
Because the module-info is missing, Gradle should put all dependencies (including all modular ones) on the classpath. When using the extra-java-module-info plugin, however, all dependencies are put on the modulepath, forcing me to define extra java module info for all transitive test dependencies.

I have found the following workaround, which adds an automatic module to each dependency.
Forcing the implementation and testImplemantation configurations to be resolvable should normally be avoided. However, runtimeClasspath, etc. cannot be used, because the missing module Exceptions are thrown during resolution.

configurations.implementation.get().isCanBeResolved = true
configurations.testImplementation.get().isCanBeResolved = true

extraJavaModuleInfo {
  listOf(
    configurations.implementation,
    configurations.testImplementation
    // Add other configurations that lead to the error here
  ).forEach {
    automaticModule(it.get())
  }
}

fun de.jjohannes.gradle.javamodules.ExtraModuleInfoPluginExtension.automaticModule(configuration: Configuration) {
  configuration.forEach {
    automaticModule(it.name, deriveModuleName(it.name))
  }
}

fun deriveModuleName(jarName: String): String {
  return "-(\\d+(\\.|$))*".toRegex().split(jarName)[0]
          .replace("[^A-Za-z0-9]".toRegex(), ".")
          .replace("\\.+".toRegex(), ".")
          .trim('.')
}

I think, this is, indeed, a feature, not a bug. This plugins allows you to control and prevent the dependency (module) hell in all of your configurations: either implementation or testImplementation.

I'm wondering why ArchUnit is causing trouble for you since com.tngtech.archunit:archunit-junit5:0.14.1 seems to be properly modularized, i.e. it has Automatic-Module-Name: com.tngtech.archunit.junit5 in MANIFEST.MF. Its transitive dependency also have Automation-Module-Name definitions.

With regards to whitebox testing, you may want to try out the approach the Project Skara team has used. In a nutshell, they created a simple Gradle plugin which extends the main module with extra "requires"/"opens" definitions required only for test code so you don't have to create a special module-info.java file for the test source set.

https://github.com/openjdk/skara/blob/master/buildSrc/module/src/main/java/org/openjdk/skara/gradle/module/ModulePlugin.java#L64

The code above line 64 is not required (i.e. modifying the main 'javaCompile' task) because Gradle 6.4 with enabled modularity will take care of this.

From a user perspective it looks as follows.

plugins {
  id 'org.openjdk.skara.gradle.module'
}

module {
    name = 'org.openjdk.skara.email'
    test {
        requires 'org.openjdk.skara.test'
        requires 'org.junit.jupiter.api'
        opens 'org.openjdk.skara.email' to 'org.junit.platform.commons'
    }
}

I see your point @mrcjkb. This plugin currently assumes that everything should become a model. But if you want to do whitebox testing on the classpath, which is a possible unit testing approach, this might make it a bit "ugly". But only if you have a dependency that is not a module that you only use for testing (like your archunit example).

But I don't want to disable the plugin functionality for tests in general. Other users might indeed do backbox testing and turn their test source set into a module. That's why I won't integrate #6.

I also think that @iherasymenko has a point. You could say that you want everything to be a module. Most will already be, because it needs to be for the production doe module path. And that's why, by default, this plugin enforces everything to be (or become) a real module.

@mrcjkb, what about the following solution for you:

  1. Turn off the strictness (available in the latest release). Then the plugin will just not touch the Jars (like your archunit) for which you do not have module information. Gradle will take care of the rest.
extraJavaModuleInfo {
    failOnMissingModuleInfo.set(false)
}
  1. Disable the features of the plugin for the test compile and runtime classpaths like this:
configurations {
    testRuntimeClasspath {
        attributes { attribute(Attribute.of("javaModule", Boolean::class.javaObjectType), false) }
    }
    testCompileClasspath {
        attributes { attribute(Attribute.of("javaModule", Boolean::class.javaObjectType), false) }
    }
}

Thanks. Your second workaround sounds like a good solution for my use case.