You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@lucene.apache.org by us...@apache.org on 2022/01/06 18:09:49 UTC

[lucene] branch branch_9x updated (336341e -> 80d057f)

This is an automated email from the ASF dual-hosted git repository.

uschindler pushed a change to branch branch_9x
in repository https://gitbox.apache.org/repos/asf/lucene.git.


    from 336341e  Revert this change as module system work was not yet backported
     new bc6aa00  LUCENE-10328: Module path for compiling and running tests is wrong (#571)
     new f568cfa  LUCENE-10328: open up certain packages for junit and the test framework (reflective access).
     new 80d057f  Revert "Revert this change as module system work was not yet backported"

The 3 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.


Summary of changes:
 gradle/documentation/render-javadoc.gradle         |  29 +-
 gradle/java/modules.gradle                         | 585 +++++++++++++++------
 gradle/maven/publications.gradle                   |   4 +-
 gradle/validation/check-environment.gradle         |  10 +-
 gradle/validation/ecj-lint.gradle                  |  49 +-
 gradle/validation/validate-source-patterns.gradle  |  14 +-
 lucene/analysis.tests/src/test/module-info.java    |   1 -
 lucene/analysis/common/build.gradle                |   2 +-
 lucene/analysis/icu/build.gradle                   |   2 +-
 lucene/analysis/kuromoji/build.gradle              |   2 +-
 lucene/analysis/morfologik.tests/build.gradle      |   5 +-
 .../morfologik.tests/src/test/module-info.java     |   2 +-
 .../morfologik/tests/TestMorfologikAnalyzer.java   |   8 +-
 lucene/analysis/morfologik/build.gradle            |   2 +-
 lucene/analysis/nori/build.gradle                  |   4 +-
 lucene/analysis/opennlp/build.gradle               |   2 +-
 lucene/analysis/phonetic/build.gradle              |   2 +-
 lucene/analysis/smartcn/build.gradle               |   2 +-
 lucene/analysis/stempel/build.gradle               |   2 +-
 lucene/backward-codecs/build.gradle                |   2 +-
 lucene/benchmark/build.gradle                      |   9 +-
 lucene/classification/build.gradle                 |   6 +-
 lucene/codecs/build.gradle                         |   2 +-
 .../lucene90/{ => tests}/MockTermStateFactory.java |  13 +-
 .../codecs/uniformsplit/TestBlockWriter.java       |   2 +-
 .../sharedterms/TestSTBlockReader.java             |   2 +-
 lucene/core.tests/build.gradle                     |   5 +-
 .../src/java/module-info.java}                     |   9 +-
 .../lucene/core/tests/main/EmptyReference.java}    |   9 +-
 .../lucene/core/tests/main}/package-info.java      |   4 +-
 .../src/java/overview.html                         |  12 +-
 lucene/core.tests/src/test/module-info.java        |   3 +-
 .../org/apache/lucene/core/tests/TestMMap.java     |   7 +-
 .../core/tests/TestModuleResourceLoader.java       |   9 +-
 .../core/tests/TestRuntimeDependenciesSane.java    |  58 ++
 lucene/core/build.gradle                           |   4 +-
 lucene/core/src/java/module-info.java              |   3 +
 lucene/demo/build.gradle                           |   2 +-
 lucene/expressions/build.gradle                    |   2 +-
 lucene/facet/build.gradle                          |   7 +-
 lucene/grouping/build.gradle                       |   2 +-
 lucene/highlighter/build.gradle                    |   6 +-
 lucene/join/build.gradle                           |   2 +-
 lucene/luke/build.gradle                           |   2 +-
 lucene/memory/build.gradle                         |   4 +-
 lucene/misc/build.gradle                           |   2 +-
 lucene/monitor/build.gradle                        |   4 +-
 lucene/queries/build.gradle                        |   4 +-
 lucene/queryparser/build.gradle                    |   2 +-
 lucene/replicator/build.gradle                     |  10 +-
 lucene/sandbox/build.gradle                        |   2 +-
 lucene/spatial-extras/build.gradle                 |  20 +-
 .../lucene/spatial/spatial4j/TestGeo3dRpt.java     |   2 +-
 .../{core => spatial-test-fixtures}/build.gradle   |   6 +-
 .../tests}/RandomGeo3dShapeGenerator.java          |  56 +-
 .../lucene/spatial3d/tests}/package-info.java      |   4 +-
 .../src/java/overview.html                         |   6 +-
 lucene/spatial3d/build.gradle                      |  10 +-
 .../spatial3d/geom/GeoBaseCompositeShape.java      |   6 +
 .../lucene/spatial3d/geom/TestGeoExactCircle.java  |   3 +-
 .../spatial3d/geom/TestRandomBinaryCodec.java      |   2 +-
 .../spatial3d/geom/TestRandomGeoPolygon.java       |   2 +-
 .../geom/TestRandomGeoShapeRelationship.java       |   2 +-
 .../lucene/spatial3d/geom/TestRandomPlane.java     |   2 +-
 lucene/suggest/build.gradle                        |   2 +-
 lucene/test-framework/src/java/module-info.java    |  10 +-
 settings.gradle                                    |   1 +
 versions.lock                                      |   2 +-
 68 files changed, 717 insertions(+), 356 deletions(-)
 rename lucene/codecs/src/test/org/apache/lucene/codecs/lucene90/{ => tests}/MockTermStateFactory.java (71%)
 copy lucene/{analysis/smartcn/src/java/org/apache/lucene/analysis/cn/smart/hhmm/package-info.java => core.tests/src/java/module-info.java} (79%)
 copy lucene/{analysis/common/src/java/org/apache/lucene/analysis/hunspell/AffixKind.java => core.tests/src/java/org/apache/lucene/core/tests/main/EmptyReference.java} (80%)
 copy lucene/{analysis/common/src/java/org/apache/lucene/analysis/ar => core.tests/src/java/org/apache/lucene/core/tests/main}/package-info.java (88%)
 copy lucene/{replicator => core.tests}/src/java/overview.html (80%)
 create mode 100644 lucene/core.tests/src/test/org/apache/lucene/core/tests/TestRuntimeDependenciesSane.java
 copy lucene/{core => spatial-test-fixtures}/build.gradle (83%)
 rename lucene/{spatial3d/src/testFixtures/java/org/apache/lucene/spatial3d/geom => spatial-test-fixtures/src/java/org/apache/lucene/spatial3d/tests}/RandomGeo3dShapeGenerator.java (95%)
 copy lucene/{analysis/common/src/java/org/apache/lucene/analysis/ar => spatial-test-fixtures/src/java/org/apache/lucene/spatial3d/tests}/package-info.java (89%)
 copy lucene/{replicator => spatial-test-fixtures}/src/java/overview.html (87%)

[lucene] 01/03: LUCENE-10328: Module path for compiling and running tests is wrong (#571)

Posted by us...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

uschindler pushed a commit to branch branch_9x
in repository https://gitbox.apache.org/repos/asf/lucene.git

commit bc6aa00c5fd7fca586ae93379bbabda60dd8273c
Author: Dawid Weiss <da...@carrotsearch.com>
AuthorDate: Wed Jan 5 20:42:02 2022 +0100

    LUCENE-10328: Module path for compiling and running tests is wrong (#571)
---
 gradle/documentation/render-javadoc.gradle         |  29 +-
 gradle/java/modules.gradle                         | 585 +++++++++++++++------
 gradle/maven/publications.gradle                   |   4 +-
 gradle/validation/check-environment.gradle         |  10 +-
 gradle/validation/ecj-lint.gradle                  |  49 +-
 gradle/validation/validate-source-patterns.gradle  |  14 +-
 lucene/analysis/common/build.gradle                |   2 +-
 lucene/analysis/icu/build.gradle                   |   2 +-
 lucene/analysis/kuromoji/build.gradle              |   2 +-
 lucene/analysis/morfologik.tests/build.gradle      |   5 +-
 .../morfologik.tests/src/test/module-info.java     |   2 +-
 .../morfologik/tests/TestMorfologikAnalyzer.java   |   8 +-
 lucene/analysis/morfologik/build.gradle            |   2 +-
 lucene/analysis/nori/build.gradle                  |   4 +-
 lucene/analysis/opennlp/build.gradle               |   2 +-
 lucene/analysis/phonetic/build.gradle              |   2 +-
 lucene/analysis/smartcn/build.gradle               |   2 +-
 lucene/analysis/stempel/build.gradle               |   2 +-
 lucene/backward-codecs/build.gradle                |   2 +-
 lucene/benchmark/build.gradle                      |   9 +-
 lucene/classification/build.gradle                 |   6 +-
 lucene/codecs/build.gradle                         |   2 +-
 .../lucene90/{ => tests}/MockTermStateFactory.java |  13 +-
 .../codecs/uniformsplit/TestBlockWriter.java       |   2 +-
 .../sharedterms/TestSTBlockReader.java             |   2 +-
 lucene/core.tests/build.gradle                     |   5 +-
 .../src/java/module-info.java}                     |  14 +-
 .../lucene/core/tests/main/EmptyReference.java}    |  13 +-
 .../lucene/core/tests/main/package-info.java}      |  10 +-
 lucene/core.tests/src/java/overview.html           |  26 +
 lucene/core.tests/src/test/module-info.java        |   3 +-
 .../org/apache/lucene/core/tests/TestMMap.java     |   7 +-
 .../core/tests/TestModuleResourceLoader.java       |   9 +-
 .../core/tests/TestRuntimeDependenciesSane.java    |  58 ++
 lucene/core/build.gradle                           |   4 +-
 lucene/demo/build.gradle                           |   2 +-
 lucene/expressions/build.gradle                    |   2 +-
 lucene/facet/build.gradle                          |   7 +-
 lucene/grouping/build.gradle                       |   2 +-
 lucene/highlighter/build.gradle                    |   6 +-
 lucene/join/build.gradle                           |   2 +-
 lucene/luke/build.gradle                           |   2 +-
 lucene/memory/build.gradle                         |   4 +-
 lucene/misc/build.gradle                           |   2 +-
 lucene/monitor/build.gradle                        |   4 +-
 lucene/queries/build.gradle                        |   4 +-
 lucene/queryparser/build.gradle                    |   2 +-
 lucene/replicator/build.gradle                     |  10 +-
 lucene/sandbox/build.gradle                        |   2 +-
 lucene/spatial-extras/build.gradle                 |  20 +-
 .../lucene/spatial/spatial4j/TestGeo3dRpt.java     |   2 +-
 .../build.gradle                                   |   7 +-
 .../tests}/RandomGeo3dShapeGenerator.java          |  56 +-
 .../lucene/spatial3d/tests/package-info.java}      |  10 +-
 .../spatial-test-fixtures/src/java/overview.html   |  24 +
 lucene/spatial3d/build.gradle                      |  10 +-
 .../spatial3d/geom/GeoBaseCompositeShape.java      |   6 +
 .../lucene/spatial3d/geom/TestGeoExactCircle.java  |   3 +-
 .../spatial3d/geom/TestRandomBinaryCodec.java      |   2 +-
 .../spatial3d/geom/TestRandomGeoPolygon.java       |   2 +-
 .../geom/TestRandomGeoShapeRelationship.java       |   2 +-
 .../lucene/spatial3d/geom/TestRandomPlane.java     |   2 +-
 lucene/suggest/build.gradle                        |   2 +-
 lucene/test-framework/src/java/module-info.java    |   6 +-
 settings.gradle                                    |   1 +
 versions.lock                                      |   2 +-
 66 files changed, 753 insertions(+), 366 deletions(-)

diff --git a/gradle/documentation/render-javadoc.gradle b/gradle/documentation/render-javadoc.gradle
index 63cfbf3..0863aab 100644
--- a/gradle/documentation/render-javadoc.gradle
+++ b/gradle/documentation/render-javadoc.gradle
@@ -59,7 +59,7 @@ allprojects {
       outputDir = project.javadoc.destinationDir
     }
 
-    if (project.path == ':lucene:luke' || project.path.endsWith(".tests")) {
+    if (project.path == ':lucene:luke' || !(project in rootProject.ext.mavenProjects)) {
       // These projects are not part of the public API so we don't render their javadocs
       // as part of the site's creation. A side-effect of this is that javadocs would not
       // be linted for these projects. To avoid this, we connect the regular javadoc task
@@ -167,26 +167,6 @@ configure(project(":lucene:test-framework")) {
   project.tasks.withType(RenderJavadocTask) {
     // TODO: fix missing javadocs
     javadocMissingLevel = "class"
-    // TODO: clean up split packages
-    javadocMissingIgnore = [
-        "org.apache.lucene.analysis",
-        "org.apache.lucene.analysis.standard",
-        "org.apache.lucene.codecs",
-        "org.apache.lucene.codecs.blockterms",
-        "org.apache.lucene.codecs.bloom",
-        "org.apache.lucene.codecs.compressing",
-        "org.apache.lucene.codecs.uniformsplit",
-        "org.apache.lucene.codecs.uniformsplit.sharedterms",
-        "org.apache.lucene.geo",
-        "org.apache.lucene.index",
-        "org.apache.lucene.search",
-        "org.apache.lucene.search.similarities",
-        "org.apache.lucene.search.spans",
-        "org.apache.lucene.store",
-        "org.apache.lucene.util",
-        "org.apache.lucene.util.automaton",
-        "org.apache.lucene.util.fst"
-    ]
   }
 }
 
@@ -197,6 +177,13 @@ configure(project(":lucene:sandbox")) {
   }
 }
 
+configure(project(":lucene:spatial-test-fixtures")) {
+  project.tasks.withType(RenderJavadocTask) {
+    // TODO: fix missing javadocs
+    javadocMissingLevel = "class"
+  }
+}
+
 configure(project(":lucene:misc")) {
   project.tasks.withType(RenderJavadocTask) {
     // TODO: fix missing javadocs
diff --git a/gradle/java/modules.gradle b/gradle/java/modules.gradle
index 5e334ab..fa75116 100644
--- a/gradle/java/modules.gradle
+++ b/gradle/java/modules.gradle
@@ -15,6 +15,8 @@
  * limitations under the License.
  */
 
+import java.nio.file.Path
+
 // Configure miscellaneous aspects required for supporting the java module system layer.
 
 // Debugging utilities.
@@ -27,189 +29,162 @@ allprojects {
       modularity.inferModulePath.set(false)
     }
 
-    // Map convention configuration names to "modular" corresponding configurations.
-    Closure<String> moduleConfigurationNameFor = { String configurationName ->
-      return "module" + configurationName.capitalize().replace("Classpath", "Path")
-    }
-
-    //
-    // For each source set, create explicit configurations for declaring modular dependencies.
-    // These "modular" configurations correspond 1:1 to Gradle's conventions but have a 'module' prefix
-    // and a capitalized remaining part of the conventional name. For example, an 'api' configuration in
-    // the main source set would have a corresponding 'moduleApi' configuration for declaring modular
-    // dependencies.
-    //
-    // Gradle's java plugin "convention" configurations extend from their modular counterparts
-    // so all dependencies end up on classpath by default for backward compatibility with other
-    // tasks and gradle infrastructure.
     //
-    // At the same time, we also know which dependencies (and their transitive graph of dependencies!)
-    // should be placed on module-path only.
-    //
-    // Note that an explicit configuration of modular dependencies also opens up the possibility of automatically
-    // validating whether the dependency configuration for a gradle project is consistent with the information in
-    // the module-info descriptor because there is a (nearly?) direct correspondence between the two:
-    //
-    // moduleApi            - 'requires transitive'
-    // moduleImplementation - 'requires'
-    // moduleCompileOnly    - 'requires static'
+    // Configure modular extensions for each source set.
     //
     project.sourceSets.all { SourceSet sourceSet ->
-      ConfigurationContainer configurations = project.configurations
-
-      // Create modular configurations for convention configurations.
-      Closure<Configuration> createModuleConfigurationForConvention = { String configurationName ->
-        Configuration conventionConfiguration = configurations.maybeCreate(configurationName)
-        Configuration moduleConfiguration = configurations.maybeCreate(moduleConfigurationNameFor(configurationName))
-        moduleConfiguration.canBeConsumed(false)
-        moduleConfiguration.canBeResolved(false)
-        conventionConfiguration.extendsFrom(moduleConfiguration)
-
-        project.logger.info("Created module configuration for '${conventionConfiguration.name}': ${moduleConfiguration.name}")
-        return moduleConfiguration
-      }
-
-      Configuration moduleApi = createModuleConfigurationForConvention(sourceSet.apiConfigurationName)
-      Configuration moduleImplementation = createModuleConfigurationForConvention(sourceSet.implementationConfigurationName)
-      moduleImplementation.extendsFrom(moduleApi)
-      Configuration moduleRuntimeOnly = createModuleConfigurationForConvention(sourceSet.runtimeOnlyConfigurationName)
-      Configuration moduleCompileOnly = createModuleConfigurationForConvention(sourceSet.compileOnlyConfigurationName)
-      // sourceSet.compileOnlyApiConfigurationName  // This seems like a very esoteric use case, leave out.
-
-      // Set up compilation module path configuration combining corresponding convention configurations.
-      Closure<Configuration> createResolvableModuleConfiguration = { String configurationName ->
-        Configuration conventionConfiguration = configurations.maybeCreate(configurationName)
-        Configuration moduleConfiguration = configurations.maybeCreate(
-            moduleConfigurationNameFor(conventionConfiguration.name))
-        moduleConfiguration.canBeConsumed(false)
-        moduleConfiguration.canBeResolved(true)
-        moduleConfiguration.attributes {
-          // Prefer class folders over JARs. The exception is made for tests projects which require a composition
-          // of classes and resources, otherwise split into two folders.
-          if (project.name.endsWith(".tests")) {
-            attribute(LibraryElements.LIBRARY_ELEMENTS_ATTRIBUTE, objects.named(LibraryElements, LibraryElements.JAR))
-          } else {
-            attribute(LibraryElements.LIBRARY_ELEMENTS_ATTRIBUTE, objects.named(LibraryElements, LibraryElements.CLASSES))
-          }
-        }
-
-        project.logger.info("Created resolvable module configuration for '${conventionConfiguration.name}': ${moduleConfiguration.name}")
-        return moduleConfiguration
-      }
-
-      Configuration compileModulePathConfiguration = createResolvableModuleConfiguration(sourceSet.compileClasspathConfigurationName)
-      compileModulePathConfiguration.extendsFrom(moduleCompileOnly, moduleImplementation)
-
-      Configuration runtimeModulePathConfiguration = createResolvableModuleConfiguration(sourceSet.runtimeClasspathConfigurationName)
-      runtimeModulePathConfiguration.extendsFrom(moduleRuntimeOnly, moduleImplementation)
-
       // Create and register a source set extension for manipulating classpath/ module-path
-      ModularPathsExtension modularPaths = new ModularPathsExtension(project, sourceSet,
-          compileModulePathConfiguration,
-          runtimeModulePathConfiguration)
+      ModularPathsExtension modularPaths = new ModularPathsExtension(project, sourceSet)
       sourceSet.extensions.add("modularPaths", modularPaths)
 
-      // Customized the JavaCompile for this source set so that it has proper module path.
+      // LUCENE-10344: We have to provide a special-case extension for ECJ because it does not
+      // support all of the module-specific javac options.
+      ModularPathsExtension modularPathsForEcj = modularPaths
+      if (sourceSet.name == SourceSet.TEST_SOURCE_SET_NAME && project.path in [
+          ":lucene:spatial-extras",
+          ":lucene:spatial3d",
+      ]) {
+        modularPathsForEcj = modularPaths.cloneWithMode(ModularPathsExtension.Mode.CLASSPATH_ONLY)
+      }
+      sourceSet.extensions.add("modularPathsForEcj", modularPathsForEcj)
+
+      // TODO: the tests of these projects currently don't compile or work in
+      // module-path mode. Make the modular paths extension use class path only.
+      if (sourceSet.name == SourceSet.TEST_SOURCE_SET_NAME && project.path in [
+        // Circular dependency between artifacts or source set outputs,
+        // causing package split issues at runtime.
+        ":lucene:core",
+        ":lucene:codecs",
+        ":lucene:test-framework",
+      ]) {
+        modularPaths.mode = ModularPathsExtension.Mode.CLASSPATH_ONLY
+      }
+
+      // Configure the JavaCompile task associated with this source set.
       tasks.named(sourceSet.getCompileJavaTaskName()).configure({ JavaCompile task ->
         task.dependsOn modularPaths.compileModulePathConfiguration
 
-        // LUCENE-10327: don't allow gradle to emit an empty sourcepath as it would break compilation.
+        // LUCENE-10327: don't allow gradle to emit an empty sourcepath as it would break
+        // compilation of modules.
         task.options.setSourcepath(sourceSet.java.sourceDirectories)
 
         // Add modular dependencies and their transitive dependencies to module path.
-        task.options.compilerArgumentProviders.add((CommandLineArgumentProvider) {
-          def modularPathFiles = modularPaths.compileModulePathConfiguration.files
-          def extraArgs = []
-          if (!modularPathFiles.isEmpty()) {
-            if (!modularPaths.hasModuleDescriptor()) {
-              // We're compiling a non-module so we'll bring everything on module path in
-              // otherwise things wouldn't be part of the resolved module graph.
-              extraArgs += ["--add-modules", "ALL-MODULE-PATH"]
-            }
-
-            extraArgs += ["--module-path", modularPathFiles.join(File.pathSeparator)]
-          }
-
-          task.logger.info("Module path for ${task.path}:\n  " + modularPathFiles.sort().join("\n  "))
-
-          return extraArgs
-        })
+        task.options.compilerArgumentProviders.add(modularPaths.compilationArguments)
 
         // LUCENE-10304: if we modify the classpath here, IntelliJ no longer sees the dependencies as compile-time
         // dependencies, don't know why.
         if (!rootProject.ext.isIdea) {
-          // Modify the default classpath by removing anything already placed on module path.
-          // This could be done in a fancier way but a set difference is just fine for us here. Use a lazy
-          // provider to delay computation of the actual path.
-          task.classpath = files({ ->
-            def trimmedClasspath = sourceSet.compileClasspath - modularPaths.compileModulePathConfiguration
-            task.logger.info("Class path for ${task.path}:\n  " + trimmedClasspath.files.sort().join("\n  "))
-            return trimmedClasspath
-          })
+          task.classpath = modularPaths.compilationClasspath
+        }
+
+        doFirst {
+          modularPaths.logCompilationPaths(logger)
         }
       })
+
+      // For source sets that contain a module descriptor, configure a jar task that combines
+      // classes and resources into a single module.
+      if (sourceSet.name != SourceSet.MAIN_SOURCE_SET_NAME) {
+        tasks.maybeCreate(sourceSet.getJarTaskName(), org.gradle.jvm.tasks.Jar).configure({
+          archiveClassifier = sourceSet.name
+          from(sourceSet.output)
+        })
+      }
+    }
+
+    // Connect modular configurations between their "test" and "main" source sets, this reflects
+    // the conventions set by the Java plugin.
+    project.configurations {
+      moduleTestApi.extendsFrom moduleApi
+      moduleTestImplementation.extendsFrom moduleImplementation
+      moduleTestRuntimeOnly.extendsFrom moduleRuntimeOnly
+      moduleTestCompileOnly.extendsFrom moduleCompileOnly
     }
 
+    // Gradle's java plugin sets the compile and runtime classpath to be a combination
+    // of configuration dependencies and source set's outputs. For source sets with modules,
+    // this leads to split class and resource folders.
     //
-    // Configure the (default) test task to use module paths.
+    // We tweak the default source set path configurations here by assembling jar task outputs
+    // of the respective source set, instead of their source set output folders. We also attach
+    // the main source set's jar to the modular test implementation configuration.
+    SourceSet mainSourceSet = project.sourceSets.getByName(SourceSet.MAIN_SOURCE_SET_NAME)
+    boolean mainIsModular = mainSourceSet.modularPaths.hasModuleDescriptor()
+    boolean mainIsEmpty = mainSourceSet.allJava.isEmpty()
+    SourceSet testSourceSet = project.sourceSets.getByName(SourceSet.TEST_SOURCE_SET_NAME)
+    boolean testIsModular = testSourceSet.modularPaths.hasModuleDescriptor()
+
+    // LUCENE-10304: if we modify the classpath here, IntelliJ no longer sees the dependencies as compile-time
+    // dependencies, don't know why.
+    if (!rootProject.ext.isIdea) {
+      def jarTask = project.tasks.getByName(mainSourceSet.getJarTaskName())
+      def testJarTask = project.tasks.getByName(testSourceSet.getJarTaskName())
+
+      // Consider various combinations of module/classpath configuration between the main and test source set.
+      if (testIsModular) {
+        if (mainIsModular || mainIsEmpty) {
+          // If the main source set is empty, skip the jar task.
+          def jarTaskOutputs = mainIsEmpty ? [] :  jarTask.outputs
+
+          // Fully modular tests - must have no split packages, proper access, etc.
+          // Work around the split classes/resources problem by adjusting classpaths to
+          // rely on JARs rather than source set output folders.
+          testSourceSet.compileClasspath = project.objects.fileCollection().from(
+              jarTaskOutputs,
+              project.configurations.getByName(testSourceSet.getCompileClasspathConfigurationName()),
+          )
+          testSourceSet.runtimeClasspath = project.objects.fileCollection().from(
+              jarTaskOutputs,
+              testJarTask.outputs,
+              project.configurations.getByName(testSourceSet.getRuntimeClasspathConfigurationName()),
+          )
+
+          project.dependencies {
+            moduleTestImplementation files(jarTaskOutputs)
+            moduleTestRuntimeOnly files(testJarTask.outputs)
+          }
+        } else {
+          // This combination simply does not make any sense (in my opinion).
+          throw GradleException("Test source set is modular and main source set is class-based, this makes no sense: " + project.path)
+        }
+      } else {
+        if (mainIsModular) {
+          // This combination is a potential candidate for patching the main sourceset's module with test classes. I could
+          // not resolve all the difficulties that arise when you try to do it though:
+          // - either a separate module descriptor is needed that opens test packages, adds dependencies via requires clauses
+          // or a series of jvm arguments (--add-reads, --add-opens, etc.) has to be generated and maintained. This is
+          // very low-level (ECJ doesn't support a full set of these instructions, for example).
+          //
+          // Fall back to classpath mode.
+        } else {
+          // This is the 'plain old classpath' mode: neither the main source set nor the test set are modular.
+        }
+      }
+    }
+
+    //
+    // Configures a Test task associated with the provided source set to use module paths.
     //
     // There is no explicit connection between source sets and test tasks so there is no way (?)
     // to do this automatically, convention-style.
-
+    //
     // This closure can be used to configure a different task, with a different source set, should we
     // have the need for it.
     Closure<Void> configureTestTaskForSourceSet = { Test task, SourceSet sourceSet ->
       task.configure {
-        Configuration modulePath = task.project.configurations.maybeCreate(
-            moduleConfigurationNameFor(sourceSet.getRuntimeClasspathConfigurationName()))
+        ModularPathsExtension modularPaths = sourceSet.modularPaths
 
-        task.dependsOn modulePath
+        dependsOn modularPaths
 
         // Add modular dependencies and their transitive dependencies to module path.
-        task.jvmArgumentProviders.add((CommandLineArgumentProvider) {
-          def extraArgs = []
-
-          // Determine whether the source set classes themselves should be appended
-          // to classpath or module path.
-          boolean sourceSetIsAModule = sourceSet.modularPaths.hasModuleDescriptor()
-
-          if (!modulePath.isEmpty() || sourceSetIsAModule) {
-            if (sourceSetIsAModule) {
-              // Add source set outputs to module path.
-              extraArgs += ["--module-path", (modulePath + sourceSet.output.classesDirs).files.join(File.pathSeparator)]
-              // Ideally, we should only add the sourceset's module here, everything else would be resolved via the
-              // module descriptor. But this would require parsing the module descriptor and may cause JVM version conflicts
-              // so keeping it simple.
-              extraArgs += ["--add-modules", "ALL-MODULE-PATH"]
-            } else {
-              extraArgs += ["--module-path", modulePath.files.join(File.pathSeparator)]
-              // In this case we're running a non-module against things on the module path so let's bring in
-              // everything on module path into the resolution graph.
-              extraArgs += ["--add-modules", "ALL-MODULE-PATH"]
-            }
-          }
+        jvmArgumentProviders.add(modularPaths.runtimeArguments)
 
-          task.logger.info("Module path for ${task.path}:\n  " + modulePath.files.sort().join("\n  "))
-
-          return extraArgs
-        })
+        // Modify the default classpath.
+        classpath = modularPaths.runtimeClasspath
 
-
-        // Modify the default classpath by removing anything already placed on module path.
-        // This could be done in a fancier way but a set difference is just fine for us here. Use a lazy
-        // provider to delay computation of the actual path.
-        task.classpath = files({ ->
-          def trimmedClasspath = sourceSet.runtimeClasspath - modulePath
-
-          boolean sourceSetIsAModule = sourceSet.modularPaths.hasModuleDescriptor()
-          if (sourceSetIsAModule) {
-            // also subtract the sourceSet's output directories.
-            trimmedClasspath = trimmedClasspath - sourceSet.output.classesDirs
-          }
-
-          task.logger.info("Class path for ${task.path}:\n  " + trimmedClasspath.files.sort().join("\n  "))
-          return trimmedClasspath
-        })
+        doFirst {
+          modularPaths.logRuntimePaths(logger)
+        }
       }
     }
 
@@ -237,19 +212,233 @@ allprojects {
 }
 
 
-class ModularPathsExtension {
+//
+// For a source set, create explicit configurations for declaring modular dependencies.
+//
+// These "modular" configurations correspond 1:1 to Gradle's conventions but have a 'module' prefix
+// and a capitalized remaining part of the conventional name. For example, an 'api' configuration in
+// the main source set would have a corresponding 'moduleApi' configuration for declaring modular
+// dependencies.
+//
+// Gradle's java plugin "convention" configurations extend from their modular counterparts
+// so all dependencies end up on classpath by default for backward compatibility with other
+// tasks and gradle infrastructure.
+//
+// At the same time, we also know which dependencies (and their transitive graph of dependencies!)
+// should be placed on module-path only.
+//
+// Note that an explicit configuration of modular dependencies also opens up the possibility of automatically
+// validating whether the dependency configuration for a gradle project is consistent with the information in
+// the module-info descriptor because there is a (nearly?) direct correspondence between the two:
+//
+// moduleApi            - 'requires transitive'
+// moduleImplementation - 'requires'
+// moduleCompileOnly    - 'requires static'
+//
+class ModularPathsExtension implements Cloneable, Iterable<Object> {
+  /**
+   * Determines how paths are split between module path and classpath.
+   */
+  enum Mode {
+    /**
+     * Dependencies and source set outputs are placed on classpath, even if declared on modular
+     * configurations. This would be the 'default' backward-compatible mode.
+     */
+    CLASSPATH_ONLY,
+
+    /**
+     * Dependencies from modular configurations are placed on module path. Source set outputs
+     * are placed on classpath.
+     */
+    DEPENDENCIES_ON_MODULE_PATH
+  }
+
   Project project
   SourceSet sourceSet
   Configuration compileModulePathConfiguration
   Configuration runtimeModulePathConfiguration
+  Configuration modulePatchOnlyConfiguration
 
-  ModularPathsExtension(Project project, SourceSet sourceSet,
-                        Configuration compileModulePathConfiguration,
-                        Configuration runtimeModulePathConfiguration) {
-    this.project =  project
+  // The mode of splitting paths for this source set.
+  Mode mode = Mode.DEPENDENCIES_ON_MODULE_PATH
+
+  // More verbose debugging for paths.
+  private boolean debugPaths
+
+  /**
+   * A list of module name - path provider entries that will be converted
+   * into {@code --patch-module} options.
+   */
+  private List<Map.Entry<String, Provider<Path>>> modulePatches = new ArrayList<>()
+
+  ModularPathsExtension(Project project, SourceSet sourceSet) {
+    this.project = project
     this.sourceSet = sourceSet
-    this.compileModulePathConfiguration = compileModulePathConfiguration
-    this.runtimeModulePathConfiguration = runtimeModulePathConfiguration
+
+    debugPaths = Boolean.parseBoolean(project.propertyOrDefault('build.debug.paths', 'false'))
+
+    ConfigurationContainer configurations = project.configurations
+
+    // Create modular configurations for gradle's java plugin convention configurations.
+    Configuration moduleApi = createModuleConfigurationForConvention(sourceSet.apiConfigurationName)
+    Configuration moduleImplementation = createModuleConfigurationForConvention(sourceSet.implementationConfigurationName)
+    Configuration moduleRuntimeOnly = createModuleConfigurationForConvention(sourceSet.runtimeOnlyConfigurationName)
+    Configuration moduleCompileOnly = createModuleConfigurationForConvention(sourceSet.compileOnlyConfigurationName)
+
+
+    // Apply hierarchy relationships to modular configurations.
+    moduleImplementation.extendsFrom(moduleApi)
+
+    // Patched modules have to end up in the implementation configuration for IDEs, which
+    // otherwise get terribly confused.
+    Configuration modulePatchOnly = createModuleConfigurationForConvention(
+        SourceSet.isMain(sourceSet) ? "patchOnly" : sourceSet.name + "PatchOnly")
+    modulePatchOnly.canBeResolved(true)
+    moduleImplementation.extendsFrom(modulePatchOnly)
+    this.modulePatchOnlyConfiguration = modulePatchOnly
+
+    // This part of convention configurations seems like a very esoteric use case, leave out for now.
+    // sourceSet.compileOnlyApiConfigurationName
+
+    // We have to ensure configurations are using assembled resources and classes (jar variant) as a single
+    // module can't be expanded into multiple folders.
+    Closure<Void> ensureJarVariant = { Configuration c ->
+      c.attributes.attribute(LibraryElements.LIBRARY_ELEMENTS_ATTRIBUTE, project.objects.named(LibraryElements, LibraryElements.JAR))
+    }
+
+    // Set up compilation module path configuration combining corresponding convention configurations.
+    Closure<Configuration> createResolvableModuleConfiguration = { String configurationName ->
+      Configuration conventionConfiguration = configurations.maybeCreate(configurationName)
+      Configuration moduleConfiguration = configurations.maybeCreate(moduleConfigurationNameFor(conventionConfiguration.name))
+      moduleConfiguration.canBeConsumed(false)
+      moduleConfiguration.canBeResolved(true)
+      ensureJarVariant(moduleConfiguration)
+
+      project.logger.info("Created resolvable module configuration for '${conventionConfiguration.name}': ${moduleConfiguration.name}")
+      return moduleConfiguration
+    }
+
+    ensureJarVariant(configurations.maybeCreate(sourceSet.compileClasspathConfigurationName))
+    ensureJarVariant(configurations.maybeCreate(sourceSet.runtimeClasspathConfigurationName))
+
+    this.compileModulePathConfiguration = createResolvableModuleConfiguration(sourceSet.compileClasspathConfigurationName)
+    compileModulePathConfiguration.extendsFrom(moduleCompileOnly, moduleImplementation)
+
+    this.runtimeModulePathConfiguration = createResolvableModuleConfiguration(sourceSet.runtimeClasspathConfigurationName)
+    runtimeModulePathConfiguration.extendsFrom(moduleRuntimeOnly, moduleImplementation)
+  }
+
+  /**
+   * Adds {@code --patch-module} option for the provided module name and the provider of a
+   * folder or JAR file.
+   *
+   * @param moduleName
+   * @param pathProvider
+   */
+  void patchModule(String moduleName, Provider<Path> pathProvider) {
+    modulePatches.add(Map.entry(moduleName, pathProvider));
+  }
+
+  private FileCollection getCompilationModulePath() {
+    if (mode == Mode.CLASSPATH_ONLY) {
+      return project.files()
+    }
+    return compileModulePathConfiguration - modulePatchOnlyConfiguration
+  }
+
+  private FileCollection getRuntimeModulePath() {
+    if (mode == Mode.CLASSPATH_ONLY) {
+      if (hasModuleDescriptor()) {
+        // The source set is itself a module.
+        throw new GradleException("Source set contains a module but classpath-only" +
+            " dependencies requested: ${project.path}, source set '${sourceSet.name}'")
+      }
+
+      return project.files()
+    }
+
+    return runtimeModulePathConfiguration - modulePatchOnlyConfiguration
+  }
+
+  FileCollection getCompilationClasspath() {
+    if (mode == Mode.CLASSPATH_ONLY) {
+      return sourceSet.compileClasspath
+    }
+
+    // Modify the default classpath by removing anything already placed on module path.
+    // Use a lazy provider to delay computation.
+    project.files({ ->
+      return sourceSet.compileClasspath - compileModulePathConfiguration - modulePatchOnlyConfiguration
+    })
+  }
+
+  CommandLineArgumentProvider getCompilationArguments() {
+    return new CommandLineArgumentProvider() {
+      @Override
+      Iterable<String> asArguments() {
+        FileCollection modulePath = ModularPathsExtension.this.compilationModulePath
+
+        if (modulePath.isEmpty()) {
+          return []
+        }
+
+        ArrayList<String> extraArgs = []
+        extraArgs += ["--module-path", modulePath.join(File.pathSeparator)]
+
+        if (!hasModuleDescriptor()) {
+          // We're compiling what appears to be a non-module source set so we'll
+          // bring everything on module path in the resolution graph,
+          // otherwise modular dependencies wouldn't be part of the resolved module graph and this
+          // would result in class-not-found compilation problems.
+          extraArgs += ["--add-modules", "ALL-MODULE-PATH"]
+        }
+
+        // Add module-patching.
+        extraArgs += getPatchModuleArguments(modulePatches)
+
+        return extraArgs
+      }
+    }
+  }
+
+  FileCollection getRuntimeClasspath() {
+    if (mode == Mode.CLASSPATH_ONLY) {
+      return sourceSet.runtimeClasspath
+    }
+
+    // Modify the default classpath by removing anything already placed on module path.
+    // Use a lazy provider to delay computation.
+    project.files({ ->
+      return sourceSet.runtimeClasspath - runtimeModulePath - modulePatchOnlyConfiguration
+    })
+  }
+
+  CommandLineArgumentProvider getRuntimeArguments() {
+    return new CommandLineArgumentProvider() {
+      @Override
+      Iterable<String> asArguments() {
+        FileCollection modulePath = ModularPathsExtension.this.runtimeModulePath
+
+        if (modulePath.isEmpty()) {
+          return []
+        }
+
+        def extraArgs = []
+
+        // Add source set outputs to module path.
+        extraArgs += ["--module-path", modulePath.files.join(File.pathSeparator)]
+
+        // Ideally, we should only add the sourceset's module here, everything else would be resolved via the
+        // module descriptor. But this would require parsing the module descriptor and may cause JVM version conflicts
+        // so keeping it simple.
+        extraArgs += ["--add-modules", "ALL-MODULE-PATH"]
+
+        // Add module-patching.
+        extraArgs += getPatchModuleArguments(modulePatches)
+
+        return extraArgs
+      }
+    }
   }
 
   boolean hasModuleDescriptor() {
@@ -257,4 +446,86 @@ class ModularPathsExtension {
         .map(dir -> new File(dir, "module-info.java"))
         .anyMatch(file -> file.exists())
   }
+
+  private List<String> getPatchModuleArguments(List<Map.Entry<String, Provider<Path>>> patches) {
+    def args = []
+    patches.each {
+      args.add("--patch-module");
+      args.add(it.key + "=" + it.value.get())
+    }
+    return args
+  }
+
+  private static String toList(FileCollection files) {
+    return files.isEmpty() ? " [empty]" : ("\n    " + files.sort().join("\n    "))
+  }
+
+  private static String toList(List<Map.Entry<String, Provider<Path>>> patches) {
+    return patches.isEmpty() ? " [empty]" : ("\n    " + patches.collect {"${it.key}=${it.value.get()}"}.join("\n    "))
+  }
+
+  public void logCompilationPaths(Logger logger) {
+    def value = "Modular extension, compilation paths, source set=${sourceSet.name}${hasModuleDescriptor() ? " (module)" : ""}, mode=${mode}:\n" +
+        "  Module path:${toList(compilationModulePath)}\n" +
+        "  Class path: ${toList(compilationClasspath)}\n" +
+        "  Patches:    ${toList(modulePatches)}"
+
+    if (debugPaths) {
+      logger.lifecycle(value)
+    } else {
+      logger.info(value)
+    }
+  }
+
+  public void logRuntimePaths(Logger logger) {
+    def value = "Modular extension, runtime paths, source set=${sourceSet.name}${hasModuleDescriptor() ? " (module)" : ""}, mode=${mode}:\n" +
+        "  Module path:${toList(runtimeModulePath)}\n" +
+        "  Class path: ${toList(runtimeClasspath)}\n" +
+        "  Patches   : ${toList(modulePatches)}"
+
+    if (debugPaths) {
+      logger.lifecycle(value)
+    } else {
+      logger.info(value)
+    }
+  }
+
+  public ModularPathsExtension clone() {
+    return (ModularPathsExtension) super.clone()
+  }
+
+  ModularPathsExtension cloneWithMode(Mode newMode) {
+    def cloned = this.clone()
+    cloned.mode = newMode
+    return cloned
+  }
+
+  // Map convention configuration names to "modular" corresponding configurations.
+  static String moduleConfigurationNameFor(String configurationName) {
+    return "module" + configurationName.capitalize().replace("Classpath", "Path")
+  }
+
+  // Create module configuration for the corresponding convention configuration.
+  private Configuration createModuleConfigurationForConvention(String configurationName) {
+    ConfigurationContainer configurations = project.configurations
+    Configuration conventionConfiguration = configurations.maybeCreate(configurationName)
+    Configuration moduleConfiguration = configurations.maybeCreate(moduleConfigurationNameFor(configurationName))
+    moduleConfiguration.canBeConsumed(false)
+    moduleConfiguration.canBeResolved(false)
+    conventionConfiguration.extendsFrom(moduleConfiguration)
+
+    project.logger.info("Created module configuration for '${conventionConfiguration.name}': ${moduleConfiguration.name}")
+    return moduleConfiguration
+  }
+
+  /**
+   * Provide internal dependencies for tasks willing to depend on this modular paths object.
+   */
+  @Override
+  Iterator<Object> iterator() {
+    return [
+        compileModulePathConfiguration,
+        runtimeModulePathConfiguration
+    ].iterator()
+  }
 }
\ No newline at end of file
diff --git a/gradle/maven/publications.gradle b/gradle/maven/publications.gradle
index cc9bc96..17b444b 100644
--- a/gradle/maven/publications.gradle
+++ b/gradle/maven/publications.gradle
@@ -38,7 +38,9 @@ configure(rootProject) {
           // Exclude the parent container project for analysis modules (no artifacts).
           ":lucene:analysis",
           // Exclude the native module.
-          ":lucene:misc:native"
+          ":lucene:misc:native",
+          // Exclude test fixtures.
+          ":lucene:spatial-test-fixtures"
       ]
 
       // Exclude all subprojects that are modular test projects and those explicitly
diff --git a/gradle/validation/check-environment.gradle b/gradle/validation/check-environment.gradle
index 7694f8d..00912c4 100644
--- a/gradle/validation/check-environment.gradle
+++ b/gradle/validation/check-environment.gradle
@@ -33,14 +33,20 @@ configure(rootProject) {
   def currentJavaVersion = JavaVersion.current()
   if (currentJavaVersion < minJavaVersion) {
     throw new GradleException("At least Java ${minJavaVersion} is required, you are running Java ${currentJavaVersion} "
-      + "[${System.getProperty('java.vm.name')} ${System.getProperty('java.vm.version')}]")
+        + "[${System.getProperty('java.vm.name')} ${System.getProperty('java.vm.version')}]")
   }
 
   // If we're regenerating the wrapper, skip the check.
   if (!gradle.startParameter.taskNames.contains("wrapper")) {
     def currentGradleVersion = GradleVersion.current()
     if (currentGradleVersion != GradleVersion.version(expectedGradleVersion)) {
-      throw new GradleException("Gradle ${expectedGradleVersion} is required (hint: use the gradlew script): this gradle is ${currentGradleVersion}")
+      if (currentGradleVersion.baseVersion == GradleVersion.version(expectedGradleVersion).baseVersion) {
+        logger.warn("Gradle ${expectedGradleVersion} is required but base version of this gradle matches, proceeding (" +
+            "this gradle is ${currentGradleVersion})")
+      } else {
+        throw new GradleException("Gradle ${expectedGradleVersion} is required (hint: use the gradlew script): " +
+            "this gradle is ${currentGradleVersion}")
+      }
     }
   }
 }
diff --git a/gradle/validation/ecj-lint.gradle b/gradle/validation/ecj-lint.gradle
index 0d99d2c..33b79ca 100644
--- a/gradle/validation/ecj-lint.gradle
+++ b/gradle/validation/ecj-lint.gradle
@@ -74,42 +74,36 @@ allprojects {
         args += [ "-properties", file("${resources}/ecj.javadocs.prefs").absolutePath ]
 
         // We depend on modular paths.
-        def modularPaths = sourceSet.modularPaths
-        dependsOn modularPaths.compileModulePathConfiguration
+        def modularPaths = sourceSet.modularPathsForEcj
+        dependsOn modularPaths
 
-        // Place input files in an external file to dodge command line argument
-        // limits. We could pass a directory but ecj seems to be buggy: when it
-        // encounters a module-info.java file it no longer compiles other source files.
-        def inputsFile = file("${tmpDst}/ecj-inputs.txt")
+        // Add modular dependencies and their transitive dependencies to module path.
+        task.argumentProviders.add(modularPaths.compilationArguments)
 
+        // Add classpath, if needed.
         task.argumentProviders.add((CommandLineArgumentProvider) {
-          // Add modular dependencies and their transitive dependencies to module path.
-          def modularPathFiles = modularPaths.compileModulePathConfiguration.files
-          def extraArgs = []
-          if (!modularPathFiles.isEmpty()) {
-            if (!modularPaths.hasModuleDescriptor()) {
-              // We're compiling a non-module so we'll bring everything on module path in
-              // otherwise things wouldn't be part of the resolved module graph.
-              extraArgs += ["--add-modules", "ALL-MODULE-PATH"]
-            }
-
-            extraArgs += ["--module-path", modularPathFiles.join(File.pathSeparator)]
-          }
-
           // Add classpath locations in a lazy provider (can't resolve the
           // configuration at evaluation time). Filter out non-existing entries
           // (output folders for non-existing input source dirs like resources).
-          def cpath = sourceSet.compileClasspath.filter { p -> p.exists() }
-          cpath = cpath - modularPathFiles
+          FileCollection cpath = modularPaths.compilationClasspath.filter { p -> p.exists() }
           if (!cpath.isEmpty()) {
-            extraArgs += ["-classpath", cpath.join(File.pathSeparator)]
+            return ["-classpath", cpath.join(File.pathSeparator)]
+          } else {
+            return []
           }
+        })
 
-          extraArgs += ["@" + inputsFile.absolutePath]
-          return extraArgs
+        // Place input files in an external file to dodge command line argument
+        // limits. We could pass a directory but ecj seems to be buggy: when it
+        // encounters a module-info.java file it no longer compiles other source files.
+        def inputsFile = file("${tmpDst}/ecj-inputs.txt")
+        task.argumentProviders.add((CommandLineArgumentProvider) {
+          return ["@" + inputsFile.absolutePath]
         })
 
         doFirst {
+          modularPaths.logCompilationPaths(logger)
+
           tmpDst.mkdirs()
 
           // escape filename accoring to ECJ's rules:
@@ -118,7 +112,12 @@ allprojects {
           def escapeFileName = { String s -> s.replaceAll(/ +/, /"$0"/) }
           inputsFile.setText(
             srcDirs.collectMany { dir ->
-              project.fileTree(dir: dir, include: "**/*.java" ).files
+              project.fileTree(
+                  dir: dir,
+                  include: "**/*.java",
+                  // Exclude the benchmark class with dependencies on nekohtml, which causes module-classpath conflicts and breaks ecj.
+                  exclude: "**/DemoHTMLParser.java"
+              ).files
             }
             // Try to sort all input files; a side-effect of this should be that module-info.java
             // is placed first on the list, which works around ECJ bug:
diff --git a/gradle/validation/validate-source-patterns.gradle b/gradle/validation/validate-source-patterns.gradle
index 3c5cf71..a82ce95 100644
--- a/gradle/validation/validate-source-patterns.gradle
+++ b/gradle/validation/validate-source-patterns.gradle
@@ -150,12 +150,11 @@ class ValidateSourcePatternsTask extends DefaultTask {
       (~$/\n\s*var\s+.*=.*<>.*/$) : 'Diamond operators should not be used with var'
     ]
 
-    def found = 0;
     def violations = new TreeSet();
     def reportViolation = { f, name ->
-      logger.error('{}: {}', name, f);
-      violations.add(name);
-      found++;
+      String msg = String.format(Locale.ROOT, "%s: %s", f, name)
+      logger.error(msg)
+      violations.add(msg)
     }
 
     def javadocsPattern = ~$/(?sm)^\Q/**\E(.*?)\Q*/\E/$;
@@ -254,9 +253,10 @@ class ValidateSourcePatternsTask extends DefaultTask {
     }
     progress.completed()
 
-    if (found) {
-      throw new GradleException(String.format(Locale.ENGLISH, 'Found %d violations in source files (%s).',
-        found, violations.join(', ')));
+    if (!violations.isEmpty()) {
+      throw new GradleException(String.format(Locale.ENGLISH, 'Found %d source violation(s):\n  %s',
+        violations.size(),
+        violations.join('\n  ')))
     }
   }
 }
diff --git a/lucene/analysis/common/build.gradle b/lucene/analysis/common/build.gradle
index 1815041..6530ac6 100644
--- a/lucene/analysis/common/build.gradle
+++ b/lucene/analysis/common/build.gradle
@@ -21,7 +21,7 @@ description = 'Analyzers for indexing content in different languages and domains
 
 dependencies {
   moduleApi project(':lucene:core')
-  testImplementation project(':lucene:test-framework')
+  moduleTestImplementation project(':lucene:test-framework')
 }
 
 // Fetch the data and enable regression tests against woorm/ libreoffice dictionaries.
diff --git a/lucene/analysis/icu/build.gradle b/lucene/analysis/icu/build.gradle
index e76b327..aba455f 100644
--- a/lucene/analysis/icu/build.gradle
+++ b/lucene/analysis/icu/build.gradle
@@ -25,5 +25,5 @@ dependencies {
 
   moduleApi 'com.ibm.icu:icu4j'
 
-  testImplementation project(':lucene:test-framework')
+  moduleTestImplementation project(':lucene:test-framework')
 }
diff --git a/lucene/analysis/kuromoji/build.gradle b/lucene/analysis/kuromoji/build.gradle
index 07fc0b0..fd8aedb 100644
--- a/lucene/analysis/kuromoji/build.gradle
+++ b/lucene/analysis/kuromoji/build.gradle
@@ -23,5 +23,5 @@ dependencies {
   moduleApi project(':lucene:core')
   moduleApi project(':lucene:analysis:common')
 
-  testImplementation project(':lucene:test-framework')
+  moduleTestImplementation project(':lucene:test-framework')
 }
diff --git a/lucene/analysis/morfologik.tests/build.gradle b/lucene/analysis/morfologik.tests/build.gradle
index 9cd6720..d2f94dd 100644
--- a/lucene/analysis/morfologik.tests/build.gradle
+++ b/lucene/analysis/morfologik.tests/build.gradle
@@ -21,8 +21,5 @@ description = 'Module tests for :lucene:analysis:morfologik'
 
 dependencies {
   moduleTestImplementation project(':lucene:analysis:morfologik')
-  moduleTestImplementation("junit:junit", {
-    exclude group: "org.hamcrest"
-  })
-  moduleTestImplementation "org.hamcrest:hamcrest"
+  moduleTestImplementation project(':lucene:test-framework')
 }
diff --git a/lucene/analysis/morfologik.tests/src/test/module-info.java b/lucene/analysis/morfologik.tests/src/test/module-info.java
index 15642a9..b6f869f 100644
--- a/lucene/analysis/morfologik.tests/src/test/module-info.java
+++ b/lucene/analysis/morfologik.tests/src/test/module-info.java
@@ -21,7 +21,7 @@ module org.apache.lucene.analysis.morfologik.tests {
   requires org.apache.lucene.core;
   requires org.apache.lucene.analysis.common;
   requires org.apache.lucene.analysis.morfologik;
-  requires junit;
+  requires org.apache.lucene.test_framework;
 
   exports org.apache.lucene.analysis.morfologik.tests;
 }
diff --git a/lucene/analysis/morfologik.tests/src/test/org/apache/lucene/analysis/morfologik/tests/TestMorfologikAnalyzer.java b/lucene/analysis/morfologik.tests/src/test/org/apache/lucene/analysis/morfologik/tests/TestMorfologikAnalyzer.java
index c2933fc..b1607d9 100644
--- a/lucene/analysis/morfologik.tests/src/test/org/apache/lucene/analysis/morfologik/tests/TestMorfologikAnalyzer.java
+++ b/lucene/analysis/morfologik.tests/src/test/org/apache/lucene/analysis/morfologik/tests/TestMorfologikAnalyzer.java
@@ -19,28 +19,24 @@ package org.apache.lucene.analysis.morfologik.tests;
 import org.apache.lucene.analysis.morfologik.MorfologikAnalyzer;
 import org.apache.lucene.analysis.uk.UkrainianMorfologikAnalyzer;
 import org.apache.lucene.index.IndexWriter;
+import org.apache.lucene.tests.util.LuceneTestCase;
 import org.junit.Assert;
-import org.junit.Test;
 
-public class TestMorfologikAnalyzer {
-  @Test
+public class TestMorfologikAnalyzer extends LuceneTestCase {
   public void testMorfologikAnalyzerLoads() {
     var analyzer = new MorfologikAnalyzer();
     Assert.assertNotNull(analyzer);
   }
 
-  @Test
   public void testUkrainianMorfologikAnalyzerLoads() {
     var analyzer = new UkrainianMorfologikAnalyzer();
     Assert.assertNotNull(analyzer);
   }
 
-  @Test
   public void testWeAreModule() {
     Assert.assertTrue(this.getClass().getModule().isNamed());
   }
 
-  @Test
   public void testLuceneIsAModule() {
     Assert.assertTrue(IndexWriter.class.getModule().isNamed());
   }
diff --git a/lucene/analysis/morfologik/build.gradle b/lucene/analysis/morfologik/build.gradle
index 03d7c09..4faee5d 100644
--- a/lucene/analysis/morfologik/build.gradle
+++ b/lucene/analysis/morfologik/build.gradle
@@ -27,5 +27,5 @@ dependencies {
   moduleImplementation 'org.carrot2:morfologik-polish'
   moduleImplementation 'ua.net.nlp:morfologik-ukrainian-search'
 
-  testImplementation project(':lucene:test-framework')
+  moduleTestImplementation project(':lucene:test-framework')
 }
diff --git a/lucene/analysis/nori/build.gradle b/lucene/analysis/nori/build.gradle
index 079cf50..c84ade8 100644
--- a/lucene/analysis/nori/build.gradle
+++ b/lucene/analysis/nori/build.gradle
@@ -22,7 +22,7 @@ description = 'Korean Morphological Analyzer'
 dependencies {
   moduleApi project(':lucene:core')
   moduleApi project(':lucene:analysis:common')
-  
-  testImplementation project(':lucene:test-framework')
+
+  moduleTestImplementation project(':lucene:test-framework')
 }
 
diff --git a/lucene/analysis/opennlp/build.gradle b/lucene/analysis/opennlp/build.gradle
index 3fee61a..2964e88 100644
--- a/lucene/analysis/opennlp/build.gradle
+++ b/lucene/analysis/opennlp/build.gradle
@@ -24,5 +24,5 @@ dependencies {
   moduleApi project(':lucene:analysis:common')
   moduleApi 'org.apache.opennlp:opennlp-tools'
 
-  testImplementation project(':lucene:test-framework')
+  moduleTestImplementation project(':lucene:test-framework')
 }
diff --git a/lucene/analysis/phonetic/build.gradle b/lucene/analysis/phonetic/build.gradle
index 2297af5..1b8eee8 100644
--- a/lucene/analysis/phonetic/build.gradle
+++ b/lucene/analysis/phonetic/build.gradle
@@ -25,6 +25,6 @@ dependencies {
 
   moduleApi 'commons-codec:commons-codec'
 
-  testImplementation project(':lucene:test-framework')
+  moduleTestImplementation project(':lucene:test-framework')
 } 
 
diff --git a/lucene/analysis/smartcn/build.gradle b/lucene/analysis/smartcn/build.gradle
index 960be70..87aafc9 100644
--- a/lucene/analysis/smartcn/build.gradle
+++ b/lucene/analysis/smartcn/build.gradle
@@ -23,5 +23,5 @@ dependencies {
   moduleApi project(':lucene:core')
   moduleApi project(':lucene:analysis:common')
 
-  testImplementation project(':lucene:test-framework')
+  moduleTestImplementation project(':lucene:test-framework')
 } 
diff --git a/lucene/analysis/stempel/build.gradle b/lucene/analysis/stempel/build.gradle
index 3299159..52d1b81 100644
--- a/lucene/analysis/stempel/build.gradle
+++ b/lucene/analysis/stempel/build.gradle
@@ -23,5 +23,5 @@ dependencies {
   moduleApi project(':lucene:core')
   moduleApi project(':lucene:analysis:common')
 
-  testImplementation project(':lucene:test-framework')
+  moduleTestImplementation project(':lucene:test-framework')
 }
diff --git a/lucene/backward-codecs/build.gradle b/lucene/backward-codecs/build.gradle
index d5e25d9..528c1a5 100644
--- a/lucene/backward-codecs/build.gradle
+++ b/lucene/backward-codecs/build.gradle
@@ -22,5 +22,5 @@ description = 'Codecs for older versions of Lucene'
 
 dependencies {
   moduleApi project(':lucene:core')
-  testImplementation project(':lucene:test-framework')
+  moduleTestImplementation project(':lucene:test-framework')
 }
diff --git a/lucene/benchmark/build.gradle b/lucene/benchmark/build.gradle
index 9da51c3..97f17bd 100644
--- a/lucene/benchmark/build.gradle
+++ b/lucene/benchmark/build.gradle
@@ -36,11 +36,18 @@ dependencies {
   moduleImplementation "org.locationtech.spatial4j:spatial4j"
   moduleImplementation ("net.sourceforge.nekohtml:nekohtml", {
     exclude module: "xml-apis"
+    // LUCENE-10337: Exclude xercesImpl from module path because it has split packages with the JDK (!)
+    exclude module: "xercesImpl"
+  })
+
+  // LUCENE-10337: Include xercesImpl on regular classpath where it won't cause conflicts.
+  implementation ("xerces:xercesImpl", {
+    exclude module: "xml-apis"
   })
 
   moduleRuntimeOnly project(':lucene:analysis:icu')
 
-  testImplementation project(':lucene:test-framework')
+  moduleTestImplementation project(':lucene:test-framework')
 }
 
 // We add 'conf' to resources because we validate *.alg script correctness in one of the tests.
diff --git a/lucene/classification/build.gradle b/lucene/classification/build.gradle
index 8566cdb..1b8a1bd 100644
--- a/lucene/classification/build.gradle
+++ b/lucene/classification/build.gradle
@@ -25,7 +25,7 @@ dependencies {
   moduleImplementation project(':lucene:queries')
   moduleImplementation project(':lucene:grouping')
 
-  testImplementation project(':lucene:test-framework')
-  testImplementation project(':lucene:analysis:common')
-  testImplementation project(':lucene:codecs')
+  moduleTestImplementation project(':lucene:test-framework')
+  moduleTestImplementation project(':lucene:analysis:common')
+  moduleTestImplementation project(':lucene:codecs')
 }
diff --git a/lucene/codecs/build.gradle b/lucene/codecs/build.gradle
index 92e0782..1de66fe 100644
--- a/lucene/codecs/build.gradle
+++ b/lucene/codecs/build.gradle
@@ -21,5 +21,5 @@ description = 'Lucene codecs and postings formats'
 
 dependencies {
     moduleImplementation project(':lucene:core')
-    testImplementation project(':lucene:test-framework')
+    moduleTestImplementation project(':lucene:test-framework')
 }
diff --git a/lucene/codecs/src/test/org/apache/lucene/codecs/lucene90/MockTermStateFactory.java b/lucene/codecs/src/test/org/apache/lucene/codecs/lucene90/tests/MockTermStateFactory.java
similarity index 71%
rename from lucene/codecs/src/test/org/apache/lucene/codecs/lucene90/MockTermStateFactory.java
rename to lucene/codecs/src/test/org/apache/lucene/codecs/lucene90/tests/MockTermStateFactory.java
index 17ee5ef..69c36e7 100644
--- a/lucene/codecs/src/test/org/apache/lucene/codecs/lucene90/MockTermStateFactory.java
+++ b/lucene/codecs/src/test/org/apache/lucene/codecs/lucene90/tests/MockTermStateFactory.java
@@ -15,13 +15,14 @@
  * limitations under the License.
  */
 
-package org.apache.lucene.codecs.lucene90;
+package org.apache.lucene.codecs.lucene90.tests;
 
-/** Test utility class to create mock {@link Lucene90PostingsFormat.IntBlockTermState}. */
-public class MockTermStateFactory {
+import org.apache.lucene.codecs.lucene90.Lucene90PostingsFormat.IntBlockTermState;
 
-  /** Creates an empty {@link Lucene90PostingsFormat.IntBlockTermState}. */
-  public static Lucene90PostingsFormat.IntBlockTermState create() {
-    return new Lucene90PostingsFormat.IntBlockTermState();
+/** Test utility class to create mock {@link IntBlockTermState}. */
+public class MockTermStateFactory {
+  /** Creates an empty {@link IntBlockTermState}. */
+  public static IntBlockTermState create() {
+    return new IntBlockTermState();
   }
 }
diff --git a/lucene/codecs/src/test/org/apache/lucene/codecs/uniformsplit/TestBlockWriter.java b/lucene/codecs/src/test/org/apache/lucene/codecs/uniformsplit/TestBlockWriter.java
index 5d74e7b..5f05cb0 100644
--- a/lucene/codecs/src/test/org/apache/lucene/codecs/uniformsplit/TestBlockWriter.java
+++ b/lucene/codecs/src/test/org/apache/lucene/codecs/uniformsplit/TestBlockWriter.java
@@ -19,7 +19,7 @@ package org.apache.lucene.codecs.uniformsplit;
 
 import java.io.IOException;
 import java.util.Collections;
-import org.apache.lucene.codecs.lucene90.MockTermStateFactory;
+import org.apache.lucene.codecs.lucene90.tests.MockTermStateFactory;
 import org.apache.lucene.index.DocValuesType;
 import org.apache.lucene.index.FieldInfo;
 import org.apache.lucene.index.IndexOptions;
diff --git a/lucene/codecs/src/test/org/apache/lucene/codecs/uniformsplit/sharedterms/TestSTBlockReader.java b/lucene/codecs/src/test/org/apache/lucene/codecs/uniformsplit/sharedterms/TestSTBlockReader.java
index 2a5e912..dc5a4c2 100644
--- a/lucene/codecs/src/test/org/apache/lucene/codecs/uniformsplit/sharedterms/TestSTBlockReader.java
+++ b/lucene/codecs/src/test/org/apache/lucene/codecs/uniformsplit/sharedterms/TestSTBlockReader.java
@@ -27,7 +27,7 @@ import java.util.Map;
 import java.util.Set;
 import org.apache.lucene.codecs.BlockTermState;
 import org.apache.lucene.codecs.PostingsReaderBase;
-import org.apache.lucene.codecs.lucene90.MockTermStateFactory;
+import org.apache.lucene.codecs.lucene90.tests.MockTermStateFactory;
 import org.apache.lucene.codecs.uniformsplit.BlockHeader;
 import org.apache.lucene.codecs.uniformsplit.BlockLine;
 import org.apache.lucene.codecs.uniformsplit.FSTDictionary;
diff --git a/lucene/core.tests/build.gradle b/lucene/core.tests/build.gradle
index 9ada300..4bc8bf5 100644
--- a/lucene/core.tests/build.gradle
+++ b/lucene/core.tests/build.gradle
@@ -21,8 +21,5 @@ description = 'Module tests for :lucene:core'
 
 dependencies {
   moduleTestImplementation project(':lucene:core')
-  moduleTestImplementation("junit:junit", {
-    exclude group: "org.hamcrest"
-  })
-  moduleTestImplementation "org.hamcrest:hamcrest"
+  moduleTestImplementation project(':lucene:test-framework')
 }
diff --git a/lucene/backward-codecs/build.gradle b/lucene/core.tests/src/java/module-info.java
similarity index 78%
copy from lucene/backward-codecs/build.gradle
copy to lucene/core.tests/src/java/module-info.java
index d5e25d9..28f8806 100644
--- a/lucene/backward-codecs/build.gradle
+++ b/lucene/core.tests/src/java/module-info.java
@@ -15,12 +15,10 @@
  * limitations under the License.
  */
 
-
-apply plugin: 'java-library'
-
-description = 'Codecs for older versions of Lucene'
-
-dependencies {
-  moduleApi project(':lucene:core')
-  testImplementation project(':lucene:test-framework')
+/**
+ * A placeholder module descriptor just so that we can check whether referencing it from the actual
+ * test module works.
+ */
+module org.apache.lucene.core.tests.main {
+  exports org.apache.lucene.core.tests.main;
 }
diff --git a/lucene/backward-codecs/build.gradle b/lucene/core.tests/src/java/org/apache/lucene/core/tests/main/EmptyReference.java
similarity index 80%
copy from lucene/backward-codecs/build.gradle
copy to lucene/core.tests/src/java/org/apache/lucene/core/tests/main/EmptyReference.java
index d5e25d9..55c367b 100644
--- a/lucene/backward-codecs/build.gradle
+++ b/lucene/core.tests/src/java/org/apache/lucene/core/tests/main/EmptyReference.java
@@ -14,13 +14,10 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+package org.apache.lucene.core.tests.main;
 
-
-apply plugin: 'java-library'
-
-description = 'Codecs for older versions of Lucene'
-
-dependencies {
-  moduleApi project(':lucene:core')
-  testImplementation project(':lucene:test-framework')
+/** Just an empty class to reference from the actual tests. */
+public class EmptyReference {
+  /** No instantiation. */
+  private EmptyReference() {}
 }
diff --git a/lucene/core/build.gradle b/lucene/core.tests/src/java/org/apache/lucene/core/tests/main/package-info.java
similarity index 81%
copy from lucene/core/build.gradle
copy to lucene/core.tests/src/java/org/apache/lucene/core/tests/main/package-info.java
index 989c57f..5b9241b 100644
--- a/lucene/core/build.gradle
+++ b/lucene/core.tests/src/java/org/apache/lucene/core/tests/main/package-info.java
@@ -15,11 +15,5 @@
  * limitations under the License.
  */
 
-apply plugin: 'java-library'
-
-description = 'Lucene core library'
-
-dependencies {
-  testImplementation project(':lucene:codecs')
-  testImplementation project(':lucene:test-framework')
-}
+/** Contains a placeholder class for referencing in tests. */
+package org.apache.lucene.core.tests.main;
diff --git a/lucene/core.tests/src/java/overview.html b/lucene/core.tests/src/java/overview.html
new file mode 100644
index 0000000..e71200a
--- /dev/null
+++ b/lucene/core.tests/src/java/overview.html
@@ -0,0 +1,26 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements.  See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You 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
+
+     http://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.
+-->
+<html>
+<head>
+  <title>Apache Lucene Core Tests</title>
+</head>
+  <body>
+
+    <p>A placeholder module descriptor just so that we can check whether
+      referencing it from the actual test module works.</p>
+  </body>
+</html>
diff --git a/lucene/core.tests/src/test/module-info.java b/lucene/core.tests/src/test/module-info.java
index 21fb271..26fa595 100644
--- a/lucene/core.tests/src/test/module-info.java
+++ b/lucene/core.tests/src/test/module-info.java
@@ -19,7 +19,8 @@
 @SuppressWarnings({"requires-automatic"})
 module org.apache.lucene.core.tests {
   requires org.apache.lucene.core;
-  requires junit;
+  requires org.apache.lucene.test_framework;
+  requires org.apache.lucene.core.tests.main;
 
   exports org.apache.lucene.core.tests;
 
diff --git a/lucene/core.tests/src/test/org/apache/lucene/core/tests/TestMMap.java b/lucene/core.tests/src/test/org/apache/lucene/core/tests/TestMMap.java
index 2324846..fcaae01 100644
--- a/lucene/core.tests/src/test/org/apache/lucene/core/tests/TestMMap.java
+++ b/lucene/core.tests/src/test/org/apache/lucene/core/tests/TestMMap.java
@@ -17,12 +17,11 @@
 package org.apache.lucene.core.tests;
 
 import org.apache.lucene.store.MMapDirectory;
+import org.apache.lucene.tests.util.LuceneTestCase;
 import org.junit.Assert;
-import org.junit.Test;
 
-public class TestMMap {
-  @Test
-  public void testUnmapSupported() throws Exception {
+public class TestMMap extends LuceneTestCase {
+  public void testUnmapSupported() {
     final Module module = MMapDirectory.class.getModule();
     Assert.assertTrue("Lucene Core is not loaded as module", module.isNamed());
     Assert.assertTrue(
diff --git a/lucene/core.tests/src/test/org/apache/lucene/core/tests/TestModuleResourceLoader.java b/lucene/core.tests/src/test/org/apache/lucene/core/tests/TestModuleResourceLoader.java
index ac111e6..edbd705 100644
--- a/lucene/core.tests/src/test/org/apache/lucene/core/tests/TestModuleResourceLoader.java
+++ b/lucene/core.tests/src/test/org/apache/lucene/core/tests/TestModuleResourceLoader.java
@@ -17,13 +17,12 @@
 package org.apache.lucene.core.tests;
 
 import java.io.IOException;
+import org.apache.lucene.tests.util.LuceneTestCase;
 import org.apache.lucene.util.ModuleResourceLoader;
 import org.apache.lucene.util.ResourceLoader;
-import org.junit.Assert;
 import org.junit.BeforeClass;
-import org.junit.Test;
 
-public class TestModuleResourceLoader extends Assert {
+public class TestModuleResourceLoader extends LuceneTestCase {
   private static final Module MODULE = TestModuleResourceLoader.class.getModule();
 
   private final ResourceLoader loader = new ModuleResourceLoader(MODULE);
@@ -33,7 +32,6 @@ public class TestModuleResourceLoader extends Assert {
     assertTrue("Test class must be in a named module", MODULE.isNamed());
   }
 
-  @Test
   public void testModuleResources() throws Exception {
     try (var stream = loader.openResource("org/apache/lucene/core/testresources/accessible.txt")) {
       stream.available();
@@ -51,8 +49,7 @@ public class TestModuleResourceLoader extends Assert {
     }
   }
 
-  @Test
-  public void testModuleClassloading() throws Exception {
+  public void testModuleClassloading() {
     assertSame(
         TestModuleResourceLoader.class,
         loader.findClass(TestModuleResourceLoader.class.getName(), Object.class));
diff --git a/lucene/core.tests/src/test/org/apache/lucene/core/tests/TestRuntimeDependenciesSane.java b/lucene/core.tests/src/test/org/apache/lucene/core/tests/TestRuntimeDependenciesSane.java
new file mode 100644
index 0000000..2a07d4b
--- /dev/null
+++ b/lucene/core.tests/src/test/org/apache/lucene/core/tests/TestRuntimeDependenciesSane.java
@@ -0,0 +1,58 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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
+ *
+ *     http://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.apache.lucene.core.tests;
+
+import org.apache.lucene.core.tests.main.EmptyReference;
+import org.apache.lucene.index.IndexWriter;
+import org.junit.Test;
+
+/** Intentionally not a subclass of {@code LuceneTestCase}. */
+public class TestRuntimeDependenciesSane {
+  @Test
+  public void testExternalDependenciesAreModules() {
+    isModule(org.junit.Test.class);
+  }
+
+  @Test
+  public void testInterProjectDependenciesAreModules() {
+    // The core Lucene should be a modular dependency.
+    isModule(IndexWriter.class);
+  }
+
+  @Test
+  public void testTestSourceSetIsAModule() {
+    // The test source set itself should be loaded as a module.
+    isModule(getClass());
+  }
+
+  @Test
+  public void testMainSourceSetIsAModule() {
+    // The test source set itself should be loaded as a module.
+    isModule(EmptyReference.class);
+  }
+
+  private static void isModule(Class<?> clazz) {
+    var module = clazz.getModule();
+    if (!module.isNamed()) {
+      throw new AssertionError(
+          "Class should be loaded from a named module: "
+              + clazz.getName()
+              + " but is instead part of: "
+              + module);
+    }
+  }
+}
diff --git a/lucene/core/build.gradle b/lucene/core/build.gradle
index 989c57f..e55c085 100644
--- a/lucene/core/build.gradle
+++ b/lucene/core/build.gradle
@@ -20,6 +20,6 @@ apply plugin: 'java-library'
 description = 'Lucene core library'
 
 dependencies {
-  testImplementation project(':lucene:codecs')
-  testImplementation project(':lucene:test-framework')
+  moduleTestImplementation project(':lucene:codecs')
+  moduleTestImplementation project(':lucene:test-framework')
 }
diff --git a/lucene/demo/build.gradle b/lucene/demo/build.gradle
index 70fd1df..a5c9469 100644
--- a/lucene/demo/build.gradle
+++ b/lucene/demo/build.gradle
@@ -27,5 +27,5 @@ dependencies {
   moduleImplementation project(':lucene:queryparser')
   moduleImplementation project(':lucene:expressions')
 
-  testImplementation project(':lucene:test-framework')
+  moduleTestImplementation project(':lucene:test-framework')
 }
diff --git a/lucene/expressions/build.gradle b/lucene/expressions/build.gradle
index 6823ae6..f9fcee6 100644
--- a/lucene/expressions/build.gradle
+++ b/lucene/expressions/build.gradle
@@ -29,5 +29,5 @@ dependencies {
   moduleImplementation 'org.ow2.asm:asm'
   moduleImplementation 'org.ow2.asm:asm-commons'
 
-  testImplementation project(':lucene:test-framework')
+  moduleTestImplementation project(':lucene:test-framework')
 }
diff --git a/lucene/facet/build.gradle b/lucene/facet/build.gradle
index 1b16f94..87b1950 100644
--- a/lucene/facet/build.gradle
+++ b/lucene/facet/build.gradle
@@ -22,11 +22,10 @@ description = 'Faceted indexing and search capabilities'
 
 dependencies {
   moduleApi project(':lucene:core')
-
   moduleImplementation 'com.carrotsearch:hppc'
 
-  testImplementation project(':lucene:test-framework')
-  testImplementation project(':lucene:queries')
+  moduleTestImplementation project(':lucene:test-framework')
+  moduleTestImplementation project(':lucene:queries')
   // Required for opening older indexes for backward compatibility tests
-  testImplementation project(':lucene:backward-codecs')
+  moduleTestImplementation project(':lucene:backward-codecs')
 }
diff --git a/lucene/grouping/build.gradle b/lucene/grouping/build.gradle
index 7035a11..5915c78 100644
--- a/lucene/grouping/build.gradle
+++ b/lucene/grouping/build.gradle
@@ -25,5 +25,5 @@ dependencies {
 
   moduleImplementation project(':lucene:queries')
 
-  testImplementation project(':lucene:test-framework')
+  moduleTestImplementation project(':lucene:test-framework')
 }
diff --git a/lucene/highlighter/build.gradle b/lucene/highlighter/build.gradle
index 105f1de..d2e7baf 100644
--- a/lucene/highlighter/build.gradle
+++ b/lucene/highlighter/build.gradle
@@ -26,7 +26,7 @@ dependencies {
   moduleImplementation project(':lucene:queries')
   moduleImplementation project(':lucene:memory')
 
-  testImplementation project(':lucene:test-framework')
-  testImplementation project(':lucene:analysis:common')
-  testImplementation project(':lucene:queryparser')
+  moduleTestImplementation project(':lucene:test-framework')
+  moduleTestImplementation project(':lucene:analysis:common')
+  moduleTestImplementation project(':lucene:queryparser')
 }
diff --git a/lucene/join/build.gradle b/lucene/join/build.gradle
index d8c2eab..840b3bc 100644
--- a/lucene/join/build.gradle
+++ b/lucene/join/build.gradle
@@ -21,5 +21,5 @@ description = 'Index-time and Query-time joins for normalized content'
 
 dependencies {
   moduleApi project(':lucene:core')
-  testImplementation project(':lucene:test-framework')
+  moduleTestImplementation project(':lucene:test-framework')
 }
\ No newline at end of file
diff --git a/lucene/luke/build.gradle b/lucene/luke/build.gradle
index 6def3e6..1504240 100644
--- a/lucene/luke/build.gradle
+++ b/lucene/luke/build.gradle
@@ -45,7 +45,7 @@ dependencies {
   moduleImplementation project(':lucene:analysis:stempel')
   moduleImplementation project(':lucene:suggest')
 
-  testImplementation project(':lucene:test-framework')
+  moduleTestImplementation project(':lucene:test-framework')
 }
 
 // Configure main class name for all JARs.
diff --git a/lucene/memory/build.gradle b/lucene/memory/build.gradle
index eb6626e..789070f 100644
--- a/lucene/memory/build.gradle
+++ b/lucene/memory/build.gradle
@@ -23,6 +23,6 @@ description = 'Single-document in-memory index implementation'
 dependencies {
   moduleApi project(':lucene:core')
 
-  testImplementation project(':lucene:test-framework')
-  testImplementation project(':lucene:queryparser')
+  moduleTestImplementation project(':lucene:test-framework')
+  moduleTestImplementation project(':lucene:queryparser')
 }
\ No newline at end of file
diff --git a/lucene/misc/build.gradle b/lucene/misc/build.gradle
index b9d7626..1c4468a 100644
--- a/lucene/misc/build.gradle
+++ b/lucene/misc/build.gradle
@@ -21,7 +21,7 @@ description = 'Index tools and other miscellaneous code'
 
 dependencies {
   moduleApi project(':lucene:core')
-  testImplementation project(':lucene:test-framework')
+  moduleTestImplementation project(':lucene:test-framework')
 
   nativeDeps project(":lucene:misc:native")
 }
\ No newline at end of file
diff --git a/lucene/monitor/build.gradle b/lucene/monitor/build.gradle
index ff6677f..83d6759 100644
--- a/lucene/monitor/build.gradle
+++ b/lucene/monitor/build.gradle
@@ -25,6 +25,6 @@ dependencies {
   moduleImplementation project(':lucene:memory')
   moduleImplementation project(':lucene:analysis:common')
 
-  testImplementation project(':lucene:queryparser')
-  testImplementation project(':lucene:test-framework')
+  moduleTestImplementation project(':lucene:queryparser')
+  moduleTestImplementation project(':lucene:test-framework')
 }
diff --git a/lucene/queries/build.gradle b/lucene/queries/build.gradle
index d0cc010..560365c 100644
--- a/lucene/queries/build.gradle
+++ b/lucene/queries/build.gradle
@@ -22,7 +22,7 @@ description = 'Filters and Queries that add to core Lucene'
 dependencies {
   moduleApi project(':lucene:core')
 
-  testImplementation project(':lucene:test-framework')
-  testImplementation project(':lucene:expressions')
+  moduleTestImplementation project(':lucene:test-framework')
+  moduleTestImplementation project(':lucene:expressions')
 }
 
diff --git a/lucene/queryparser/build.gradle b/lucene/queryparser/build.gradle
index b50b565..2390cfe 100644
--- a/lucene/queryparser/build.gradle
+++ b/lucene/queryparser/build.gradle
@@ -24,5 +24,5 @@ dependencies {
   moduleApi project(':lucene:queries')
   moduleApi project(':lucene:sandbox')
 
-  testImplementation project(':lucene:test-framework')
+  moduleTestImplementation project(':lucene:test-framework')
 }
diff --git a/lucene/replicator/build.gradle b/lucene/replicator/build.gradle
index d3dfc07..2058502 100644
--- a/lucene/replicator/build.gradle
+++ b/lucene/replicator/build.gradle
@@ -30,12 +30,12 @@ dependencies {
 
   moduleImplementation 'javax.servlet:javax.servlet-api'
 
-  testImplementation 'org.eclipse.jetty:jetty-server'
-  testImplementation('org.eclipse.jetty:jetty-servlet', {
+  moduleTestImplementation project(':lucene:test-framework')
+
+  moduleTestImplementation 'org.eclipse.jetty:jetty-server'
+  moduleTestImplementation('org.eclipse.jetty:jetty-servlet', {
     exclude group: "org.eclipse.jetty", module: "jetty-security"
   })
-  testImplementation 'org.eclipse.jetty:jetty-continuation'
-
-  testImplementation project(':lucene:test-framework')
+  moduleTestImplementation 'org.eclipse.jetty:jetty-continuation'
 }
 
diff --git a/lucene/sandbox/build.gradle b/lucene/sandbox/build.gradle
index b7e8018..93f01e3 100644
--- a/lucene/sandbox/build.gradle
+++ b/lucene/sandbox/build.gradle
@@ -22,5 +22,5 @@ description = 'Various third party contributions and new ideas'
 dependencies {
   moduleApi project(':lucene:core')
   moduleApi project(':lucene:queries')
-  testImplementation project(':lucene:test-framework')
+  moduleTestImplementation project(':lucene:test-framework')
 }
diff --git a/lucene/spatial-extras/build.gradle b/lucene/spatial-extras/build.gradle
index 34fa10e..baa772f 100644
--- a/lucene/spatial-extras/build.gradle
+++ b/lucene/spatial-extras/build.gradle
@@ -19,6 +19,10 @@ apply plugin: 'java-library'
 
 description = 'Geospatial search'
 
+configurations {
+  spatial4jTestPatch
+}
+
 dependencies {
   moduleApi project(':lucene:core')
   moduleApi project(':lucene:spatial3d')
@@ -26,11 +30,17 @@ dependencies {
   moduleApi 'org.locationtech.spatial4j:spatial4j'
   moduleApi 'io.sgr:s2-geometry-library-java'
 
-  testImplementation project(':lucene:test-framework')
-  testImplementation testFixtures(project(':lucene:spatial3d'))
-
+  moduleTestImplementation project(':lucene:test-framework')
+  moduleTestImplementation project(':lucene:spatial-test-fixtures')
   moduleTestImplementation 'org.locationtech.jts:jts-core'
-  testImplementation 'org.locationtech.spatial4j:spatial4j::tests'
-}
 
+  // We add patched modules to this configuration because otherwise IDEs would not see the
+  // dependency at all, even in classpath mode (they don't see --patch-module commands we
+  // add to the compiler and test tasks).
+  moduleTestPatchOnly 'org.locationtech.spatial4j:spatial4j::tests'
+  spatial4jTestPatch 'org.locationtech.spatial4j:spatial4j::tests'
+}
 
+sourceSets.test.extensions.configure("modularPaths", {
+  it.patchModule("spatial4j", project.providers.provider({ configurations.spatial4jTestPatch.singleFile }))
+})
\ No newline at end of file
diff --git a/lucene/spatial-extras/src/test/org/apache/lucene/spatial/spatial4j/TestGeo3dRpt.java b/lucene/spatial-extras/src/test/org/apache/lucene/spatial/spatial4j/TestGeo3dRpt.java
index 425142e..0d3e812 100644
--- a/lucene/spatial-extras/src/test/org/apache/lucene/spatial/spatial4j/TestGeo3dRpt.java
+++ b/lucene/spatial-extras/src/test/org/apache/lucene/spatial/spatial4j/TestGeo3dRpt.java
@@ -16,7 +16,7 @@
  */
 package org.apache.lucene.spatial.spatial4j;
 
-import static org.apache.lucene.spatial3d.geom.RandomGeo3dShapeGenerator.*;
+import static org.apache.lucene.spatial3d.tests.RandomGeo3dShapeGenerator.*;
 import static org.locationtech.spatial4j.distance.DistanceUtils.DEGREES_TO_RADIANS;
 
 import java.io.IOException;
diff --git a/lucene/backward-codecs/build.gradle b/lucene/spatial-test-fixtures/build.gradle
similarity index 83%
copy from lucene/backward-codecs/build.gradle
copy to lucene/spatial-test-fixtures/build.gradle
index d5e25d9..983416d 100644
--- a/lucene/backward-codecs/build.gradle
+++ b/lucene/spatial-test-fixtures/build.gradle
@@ -15,12 +15,11 @@
  * limitations under the License.
  */
 
-
 apply plugin: 'java-library'
 
-description = 'Codecs for older versions of Lucene'
+description = '3D spatial planar geometry test fixtures'
 
 dependencies {
-  moduleApi project(':lucene:core')
-  testImplementation project(':lucene:test-framework')
+  moduleImplementation project(':lucene:test-framework')
+  moduleImplementation project(':lucene:spatial3d')
 }
diff --git a/lucene/spatial3d/src/testFixtures/java/org/apache/lucene/spatial3d/geom/RandomGeo3dShapeGenerator.java b/lucene/spatial-test-fixtures/src/java/org/apache/lucene/spatial3d/tests/RandomGeo3dShapeGenerator.java
similarity index 95%
rename from lucene/spatial3d/src/testFixtures/java/org/apache/lucene/spatial3d/geom/RandomGeo3dShapeGenerator.java
rename to lucene/spatial-test-fixtures/src/java/org/apache/lucene/spatial3d/tests/RandomGeo3dShapeGenerator.java
index da4ac89..d3ddb3f 100644
--- a/lucene/spatial3d/src/testFixtures/java/org/apache/lucene/spatial3d/geom/RandomGeo3dShapeGenerator.java
+++ b/lucene/spatial-test-fixtures/src/java/org/apache/lucene/spatial3d/tests/RandomGeo3dShapeGenerator.java
@@ -15,7 +15,7 @@
  * limitations under the License.
  */
 
-package org.apache.lucene.spatial3d.geom;
+package org.apache.lucene.spatial3d.tests;
 
 import com.carrotsearch.randomizedtesting.RandomizedContext;
 import com.carrotsearch.randomizedtesting.RandomizedTest;
@@ -25,6 +25,23 @@ import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.Random;
+import org.apache.lucene.spatial3d.geom.GeoArea;
+import org.apache.lucene.spatial3d.geom.GeoAreaShape;
+import org.apache.lucene.spatial3d.geom.GeoBBox;
+import org.apache.lucene.spatial3d.geom.GeoBBoxFactory;
+import org.apache.lucene.spatial3d.geom.GeoCircle;
+import org.apache.lucene.spatial3d.geom.GeoCircleFactory;
+import org.apache.lucene.spatial3d.geom.GeoCompositeAreaShape;
+import org.apache.lucene.spatial3d.geom.GeoCompositeMembershipShape;
+import org.apache.lucene.spatial3d.geom.GeoPath;
+import org.apache.lucene.spatial3d.geom.GeoPathFactory;
+import org.apache.lucene.spatial3d.geom.GeoPoint;
+import org.apache.lucene.spatial3d.geom.GeoPointShape;
+import org.apache.lucene.spatial3d.geom.GeoPointShapeFactory;
+import org.apache.lucene.spatial3d.geom.GeoPolygon;
+import org.apache.lucene.spatial3d.geom.GeoPolygonFactory;
+import org.apache.lucene.spatial3d.geom.GeoShape;
+import org.apache.lucene.spatial3d.geom.PlanetModel;
 
 /**
  * Class for generating random Geo3dShapes. They can be generated under given constraints which are
@@ -42,22 +59,22 @@ public final class RandomGeo3dShapeGenerator {
   private static final int MAX_POINT_ITERATIONS = 1000;
 
   /* Supported shapes */
-  protected static final int CONVEX_POLYGON = 0;
-  protected static final int CONVEX_POLYGON_WITH_HOLES = 1;
-  protected static final int CONCAVE_POLYGON = 2;
-  protected static final int CONCAVE_POLYGON_WITH_HOLES = 3;
-  protected static final int COMPLEX_POLYGON = 4;
-  protected static final int CIRCLE = 5;
-  protected static final int RECTANGLE = 6;
-  protected static final int PATH = 7;
-  protected static final int COLLECTION = 8;
-  protected static final int POINT = 9;
-  protected static final int LINE = 10;
-  protected static final int EXACT_CIRCLE = 11;
+  public static final int CONVEX_POLYGON = 0;
+  public static final int CONVEX_POLYGON_WITH_HOLES = 1;
+  public static final int CONCAVE_POLYGON = 2;
+  public static final int CONCAVE_POLYGON_WITH_HOLES = 3;
+  public static final int COMPLEX_POLYGON = 4;
+  public static final int CIRCLE = 5;
+  public static final int RECTANGLE = 6;
+  public static final int PATH = 7;
+  public static final int COLLECTION = 8;
+  public static final int POINT = 9;
+  public static final int LINE = 10;
+  public static final int EXACT_CIRCLE = 11;
 
   /* Helper shapes for generating constraints whch are just three sided polygons */
-  protected static final int CONVEX_SIMPLE_POLYGON = 500;
-  protected static final int CONCAVE_SIMPLE_POLYGON = 501;
+  public static final int CONVEX_SIMPLE_POLYGON = 500;
+  public static final int CONCAVE_SIMPLE_POLYGON = 501;
 
   /** Static methods only. */
   private RandomGeo3dShapeGenerator() {}
@@ -550,7 +567,7 @@ public final class RandomGeo3dShapeGenerator {
           collection.addShape(member);
         }
       }
-      if (collection.shapes.size() == 0) {
+      if (collection.size() == 0) {
         continue;
       }
       return collection;
@@ -963,7 +980,7 @@ public final class RandomGeo3dShapeGenerator {
    * @param points The points to order.
    * @return The list of ordered points anti-clockwise.
    */
-  protected static List<GeoPoint> orderPoints(List<GeoPoint> points) {
+  public static List<GeoPoint> orderPoints(List<GeoPoint> points) {
     double x = 0;
     double y = 0;
     double z = 0;
@@ -1005,8 +1022,7 @@ public final class RandomGeo3dShapeGenerator {
    * Class that holds the constraints that are given to build shapes. It consists in a list of
    * GeoAreaShapes and relationships the new shape needs to satisfy.
    */
-  static class Constraints extends HashMap<GeoAreaShape, Integer> {
-
+  public static class Constraints extends HashMap<GeoAreaShape, Integer> {
     /**
      * Check if the shape is valid under the constraints.
      *
@@ -1052,7 +1068,7 @@ public final class RandomGeo3dShapeGenerator {
       // For GeoCompositeMembershipShape we only consider the first shape to help
       // converging
       if (relationship == GeoArea.WITHIN && shape instanceof GeoCompositeMembershipShape) {
-        shape = (((GeoCompositeMembershipShape) shape).shapes.get(0));
+        shape = (((GeoCompositeMembershipShape) shape).getShapes().get(0));
       }
       switch (relationship) {
         case GeoArea.DISJOINT:
diff --git a/lucene/core/build.gradle b/lucene/spatial-test-fixtures/src/java/org/apache/lucene/spatial3d/tests/package-info.java
similarity index 81%
copy from lucene/core/build.gradle
copy to lucene/spatial-test-fixtures/src/java/org/apache/lucene/spatial3d/tests/package-info.java
index 989c57f..004a77b 100644
--- a/lucene/core/build.gradle
+++ b/lucene/spatial-test-fixtures/src/java/org/apache/lucene/spatial3d/tests/package-info.java
@@ -15,11 +15,5 @@
  * limitations under the License.
  */
 
-apply plugin: 'java-library'
-
-description = 'Lucene core library'
-
-dependencies {
-  testImplementation project(':lucene:codecs')
-  testImplementation project(':lucene:test-framework')
-}
+/** Shared test fixtures for spatial packages. */
+package org.apache.lucene.spatial3d.tests;
diff --git a/lucene/spatial-test-fixtures/src/java/overview.html b/lucene/spatial-test-fixtures/src/java/overview.html
new file mode 100644
index 0000000..08e16bc
--- /dev/null
+++ b/lucene/spatial-test-fixtures/src/java/overview.html
@@ -0,0 +1,24 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements.  See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You 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
+
+     http://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.
+-->
+<html>
+  <head>
+    <title>Apache Lucene Spatial Test Fixtures Module</title>
+  </head>
+  <body>
+    <h1>Internal test fixtures for spatial modules</h1>
+  </body>
+</html>
diff --git a/lucene/spatial3d/build.gradle b/lucene/spatial3d/build.gradle
index 219d029..0b25833 100644
--- a/lucene/spatial3d/build.gradle
+++ b/lucene/spatial3d/build.gradle
@@ -16,13 +16,17 @@
  */
 
 apply plugin: 'java-library'
-apply plugin: 'java-test-fixtures'
 
 description = '3D spatial planar geometry APIs'
 
 dependencies {
   moduleApi project(':lucene:core')
+  moduleTestImplementation project(':lucene:test-framework')
 
-  testFixturesApi project(':lucene:test-framework')
-  testImplementation project(':lucene:test-framework')
+  // We have to exclude ourselves because spatial-test-fixtures depend
+  // on the main source set of this project and tests would receive the
+  // dependency twice - on classpath and in module path.
+  moduleTestImplementation(project(':lucene:spatial-test-fixtures'), {
+    exclude module: "spatial3d"
+  })
 }
diff --git a/lucene/spatial3d/src/java/org/apache/lucene/spatial3d/geom/GeoBaseCompositeShape.java b/lucene/spatial3d/src/java/org/apache/lucene/spatial3d/geom/GeoBaseCompositeShape.java
index 8c87706..876882a 100644
--- a/lucene/spatial3d/src/java/org/apache/lucene/spatial3d/geom/GeoBaseCompositeShape.java
+++ b/lucene/spatial3d/src/java/org/apache/lucene/spatial3d/geom/GeoBaseCompositeShape.java
@@ -22,6 +22,7 @@ import java.io.InputStream;
 import java.io.OutputStream;
 import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.Collections;
 import java.util.List;
 
 /**
@@ -149,4 +150,9 @@ public abstract class GeoBaseCompositeShape<T extends GeoShape> extends BasePlan
     GeoBaseCompositeShape<?> other = (GeoBaseCompositeShape<?>) o;
     return super.equals(other) && shapes.equals(other.shapes);
   }
+
+  /** Returns an unmodifiable list of composed shapes. */
+  public List<T> getShapes() {
+    return Collections.unmodifiableList(shapes);
+  }
 }
diff --git a/lucene/spatial3d/src/test/org/apache/lucene/spatial3d/geom/TestGeoExactCircle.java b/lucene/spatial3d/src/test/org/apache/lucene/spatial3d/geom/TestGeoExactCircle.java
index c99f602..28036e2 100644
--- a/lucene/spatial3d/src/test/org/apache/lucene/spatial3d/geom/TestGeoExactCircle.java
+++ b/lucene/spatial3d/src/test/org/apache/lucene/spatial3d/geom/TestGeoExactCircle.java
@@ -17,9 +17,10 @@
 
 package org.apache.lucene.spatial3d.geom;
 
-import static org.apache.lucene.spatial3d.geom.RandomGeo3dShapeGenerator.*;
+import static org.apache.lucene.spatial3d.tests.RandomGeo3dShapeGenerator.*;
 
 import com.carrotsearch.randomizedtesting.annotations.Repeat;
+import org.apache.lucene.spatial3d.tests.RandomGeo3dShapeGenerator;
 import org.apache.lucene.tests.util.LuceneTestCase;
 import org.junit.Test;
 
diff --git a/lucene/spatial3d/src/test/org/apache/lucene/spatial3d/geom/TestRandomBinaryCodec.java b/lucene/spatial3d/src/test/org/apache/lucene/spatial3d/geom/TestRandomBinaryCodec.java
index 214ef7d..3c0a6a8 100644
--- a/lucene/spatial3d/src/test/org/apache/lucene/spatial3d/geom/TestRandomBinaryCodec.java
+++ b/lucene/spatial3d/src/test/org/apache/lucene/spatial3d/geom/TestRandomBinaryCodec.java
@@ -17,7 +17,7 @@
 
 package org.apache.lucene.spatial3d.geom;
 
-import static org.apache.lucene.spatial3d.geom.RandomGeo3dShapeGenerator.*;
+import static org.apache.lucene.spatial3d.tests.RandomGeo3dShapeGenerator.*;
 
 import com.carrotsearch.randomizedtesting.annotations.Repeat;
 import java.io.ByteArrayInputStream;
diff --git a/lucene/spatial3d/src/test/org/apache/lucene/spatial3d/geom/TestRandomGeoPolygon.java b/lucene/spatial3d/src/test/org/apache/lucene/spatial3d/geom/TestRandomGeoPolygon.java
index e5a9d7d..ac87144 100644
--- a/lucene/spatial3d/src/test/org/apache/lucene/spatial3d/geom/TestRandomGeoPolygon.java
+++ b/lucene/spatial3d/src/test/org/apache/lucene/spatial3d/geom/TestRandomGeoPolygon.java
@@ -16,7 +16,7 @@
  */
 package org.apache.lucene.spatial3d.geom;
 
-import static org.apache.lucene.spatial3d.geom.RandomGeo3dShapeGenerator.*;
+import static org.apache.lucene.spatial3d.tests.RandomGeo3dShapeGenerator.*;
 
 import com.carrotsearch.randomizedtesting.generators.BiasedNumbers;
 import java.util.ArrayList;
diff --git a/lucene/spatial3d/src/test/org/apache/lucene/spatial3d/geom/TestRandomGeoShapeRelationship.java b/lucene/spatial3d/src/test/org/apache/lucene/spatial3d/geom/TestRandomGeoShapeRelationship.java
index 9f3bd75..9b63b4e 100644
--- a/lucene/spatial3d/src/test/org/apache/lucene/spatial3d/geom/TestRandomGeoShapeRelationship.java
+++ b/lucene/spatial3d/src/test/org/apache/lucene/spatial3d/geom/TestRandomGeoShapeRelationship.java
@@ -17,7 +17,7 @@
 
 package org.apache.lucene.spatial3d.geom;
 
-import static org.apache.lucene.spatial3d.geom.RandomGeo3dShapeGenerator.*;
+import static org.apache.lucene.spatial3d.tests.RandomGeo3dShapeGenerator.*;
 
 import org.apache.lucene.tests.util.LuceneTestCase;
 import org.junit.Test;
diff --git a/lucene/spatial3d/src/test/org/apache/lucene/spatial3d/geom/TestRandomPlane.java b/lucene/spatial3d/src/test/org/apache/lucene/spatial3d/geom/TestRandomPlane.java
index 2ed0066..503188a 100644
--- a/lucene/spatial3d/src/test/org/apache/lucene/spatial3d/geom/TestRandomPlane.java
+++ b/lucene/spatial3d/src/test/org/apache/lucene/spatial3d/geom/TestRandomPlane.java
@@ -17,7 +17,7 @@
 
 package org.apache.lucene.spatial3d.geom;
 
-import static org.apache.lucene.spatial3d.geom.RandomGeo3dShapeGenerator.*;
+import static org.apache.lucene.spatial3d.tests.RandomGeo3dShapeGenerator.*;
 
 import com.carrotsearch.randomizedtesting.annotations.Repeat;
 import java.util.ArrayList;
diff --git a/lucene/suggest/build.gradle b/lucene/suggest/build.gradle
index 3afccd8..25a444d 100644
--- a/lucene/suggest/build.gradle
+++ b/lucene/suggest/build.gradle
@@ -23,5 +23,5 @@ dependencies {
   moduleApi project(':lucene:core')
   moduleApi project(':lucene:analysis:common')
   
-  testImplementation project(':lucene:test-framework')
+  moduleTestImplementation project(':lucene:test-framework')
 }
diff --git a/lucene/test-framework/src/java/module-info.java b/lucene/test-framework/src/java/module-info.java
index 81ee062..d87d56a 100644
--- a/lucene/test-framework/src/java/module-info.java
+++ b/lucene/test-framework/src/java/module-info.java
@@ -16,12 +16,12 @@
  */
 
 /** Lucene test framework. */
-@SuppressWarnings({"module", "requires-automatic"})
+@SuppressWarnings({"module", "requires-automatic", "requires-transitive-automatic"})
 module org.apache.lucene.test_framework {
   requires org.apache.lucene.core;
   requires org.apache.lucene.codecs;
-  requires junit;
-  requires randomizedtesting.runner;
+  requires transitive junit;
+  requires transitive randomizedtesting.runner;
 
   exports org.apache.lucene.tests.analysis.standard;
   exports org.apache.lucene.tests.analysis;
diff --git a/settings.gradle b/settings.gradle
index 0923e9d..971fbb6 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -63,5 +63,6 @@ include "lucene:replicator"
 include "lucene:sandbox"
 include "lucene:spatial3d"
 include "lucene:spatial-extras"
+include "lucene:spatial-test-fixtures"
 include "lucene:suggest"
 include "lucene:test-framework"
diff --git a/versions.lock b/versions.lock
index c596e6e..fb12bb6 100644
--- a/versions.lock
+++ b/versions.lock
@@ -23,7 +23,7 @@ org.ow2.asm:asm-analysis:7.2 (1 constraints: e409d9a5)
 org.ow2.asm:asm-commons:7.2 (1 constraints: ad042e2c)
 org.ow2.asm:asm-tree:7.2 (2 constraints: 2f14468c)
 ua.net.nlp:morfologik-ukrainian-search:4.9.1 (1 constraints: 10051b36)
-xerces:xercesImpl:2.12.0 (2 constraints: 1f14b675)
+xerces:xercesImpl:2.12.0 (1 constraints: 3705353b)
 
 [Test dependencies]
 org.assertj:assertj-core:3.21.0 (1 constraints: 38053c3b)

[lucene] 02/03: LUCENE-10328: open up certain packages for junit and the test framework (reflective access).

Posted by us...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

uschindler pushed a commit to branch branch_9x
in repository https://gitbox.apache.org/repos/asf/lucene.git

commit f568cfa15aa278106e7f2627bb98c6da004a2553
Author: Dawid Weiss <da...@carrotsearch.com>
AuthorDate: Wed Jan 5 21:01:18 2022 +0100

    LUCENE-10328: open up certain packages for junit and the test framework (reflective access).
---
 lucene/core/src/java/module-info.java           | 3 +++
 lucene/test-framework/src/java/module-info.java | 4 ++++
 2 files changed, 7 insertions(+)

diff --git a/lucene/core/src/java/module-info.java b/lucene/core/src/java/module-info.java
index f56b194..02fc3ff 100644
--- a/lucene/core/src/java/module-info.java
+++ b/lucene/core/src/java/module-info.java
@@ -52,6 +52,9 @@ module org.apache.lucene.core {
   // Only export internal packages to the test framework.
   exports org.apache.lucene.internal.tests to
       org.apache.lucene.test_framework;
+  // Open certain packages for the test framework (ram usage tester).
+  opens org.apache.lucene.document to
+      org.apache.lucene.test_framework;
 
   provides org.apache.lucene.analysis.TokenizerFactory with
       org.apache.lucene.analysis.standard.StandardTokenizerFactory;
diff --git a/lucene/test-framework/src/java/module-info.java b/lucene/test-framework/src/java/module-info.java
index d87d56a..893d571 100644
--- a/lucene/test-framework/src/java/module-info.java
+++ b/lucene/test-framework/src/java/module-info.java
@@ -23,6 +23,10 @@ module org.apache.lucene.test_framework {
   requires transitive junit;
   requires transitive randomizedtesting.runner;
 
+  // Open certain packages for junit because it scans methods via reflection.
+  opens org.apache.lucene.tests.index to
+      junit;
+
   exports org.apache.lucene.tests.analysis.standard;
   exports org.apache.lucene.tests.analysis;
   exports org.apache.lucene.tests.codecs.asserting;

[lucene] 03/03: Revert "Revert this change as module system work was not yet backported"

Posted by us...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

uschindler pushed a commit to branch branch_9x
in repository https://gitbox.apache.org/repos/asf/lucene.git

commit 80d057fb0346b41796f395511c6024f600b5dd1e
Author: Uwe Schindler <us...@apache.org>
AuthorDate: Thu Jan 6 19:06:30 2022 +0100

    Revert "Revert this change as module system work was not yet backported"
    
    This reverts commit 336341ed71417a923881e9a8ad9c2725bb158f59.
---
 lucene/analysis.tests/src/test/module-info.java | 1 -
 1 file changed, 1 deletion(-)

diff --git a/lucene/analysis.tests/src/test/module-info.java b/lucene/analysis.tests/src/test/module-info.java
index 5026116..3a67c75 100644
--- a/lucene/analysis.tests/src/test/module-info.java
+++ b/lucene/analysis.tests/src/test/module-info.java
@@ -33,7 +33,6 @@ module org.apache.lucene.analysis.tests {
   requires org.apache.lucene.analysis.smartcn;
   requires org.apache.lucene.analysis.stempel;
   requires org.apache.lucene.test_framework;
-  requires junit;
 
   exports org.apache.lucene.analysis.tests;
 }