Production code and test code in separate JDK 9 modules?

Hi,
I’m trying to create a multi-project build which only produces and consumes JDK 9 modules. I’d like to put the production code and the tests in separate modules (gradle projects). I’ve created a very simple example that demonstrates what I’m trying to achieve, see foo.zip.
The problem is it doesn’t work :slight_smile:
So I have one module with the production code and one module with the test code. When I try to run gradle build I get the following:

FAILURE: Build failed with an exception.

* What went wrong:
Execution failed for task ':foo-test:test'.
> superClassName is empty!

For a quick reference build.gradle of the test module looks like this:

plugins {
    id 'org.gradle.java.experimental-jigsaw' version '0.1.1'
}

javaModule.name = 'foo.test'

dependencies {
    compile project(":foo-core")
    testCompile "junit:junit:4.12"
}

I have no idea what this error means, never seen it. I can’t find anything on the web either.
What do you guys think I should do?

Cheers!

Here’s the stacktrace:

org.gradle.api.tasks.TaskExecutionException: Execution failed for task ':foo-test:test'.
        at org.gradle.api.internal.tasks.execution.ExecuteActionsTaskExecuter.executeActions(ExecuteActionsTaskExecuter.java:100)
        at org.gradle.api.internal.tasks.execution.ExecuteActionsTaskExecuter.execute(ExecuteActionsTaskExecuter.java:70)
        at org.gradle.api.internal.tasks.execution.OutputDirectoryCreatingTaskExecuter.execute(OutputDirectoryCreatingTaskExecuter.java:51)
        at org.gradle.api.internal.tasks.execution.SkipUpToDateTaskExecuter.execute(SkipUpToDateTaskExecuter.java:62)
        at org.gradle.api.internal.tasks.execution.ResolveTaskOutputCachingStateExecuter.execute(ResolveTaskOutputCachingStateExecuter.java:54)
        at org.gradle.api.internal.tasks.execution.ValidatingTaskExecuter.execute(ValidatingTaskExecuter.java:60)
        at org.gradle.api.internal.tasks.execution.SkipEmptySourceFilesTaskExecuter.execute(SkipEmptySourceFilesTaskExecuter.java:97)
        at org.gradle.api.internal.tasks.execution.CleanupStaleOutputsExecuter.execute(CleanupStaleOutputsExecuter.java:87)
        at org.gradle.api.internal.tasks.execution.ResolveTaskArtifactStateTaskExecuter.execute(ResolveTaskArtifactStateTaskExecuter.java:52)
        at org.gradle.api.internal.tasks.execution.SkipTaskWithNoActionsExecuter.execute(SkipTaskWithNoActionsExecuter.java:52)
        at org.gradle.api.internal.tasks.execution.SkipOnlyIfTaskExecuter.execute(SkipOnlyIfTaskExecuter.java:54)
        at org.gradle.api.internal.tasks.execution.ExecuteAtMostOnceTaskExecuter.execute(ExecuteAtMostOnceTaskExecuter.java:43)
        at org.gradle.api.internal.tasks.execution.CatchExceptionTaskExecuter.execute(CatchExceptionTaskExecuter.java:34)
        at org.gradle.execution.taskgraph.DefaultTaskGraphExecuter$EventFiringTaskWorker$1.run(DefaultTaskGraphExecuter.java:248)
        at org.gradle.internal.progress.DefaultBuildOperationExecutor$RunnableBuildOperationWorker.execute(DefaultBuildOperationExecutor.java:336)
        at org.gradle.internal.progress.DefaultBuildOperationExecutor$RunnableBuildOperationWorker.execute(DefaultBuildOperationExecutor.java:328)
        at org.gradle.internal.progress.DefaultBuildOperationExecutor.execute(DefaultBuildOperationExecutor.java:199)
        at org.gradle.internal.progress.DefaultBuildOperationExecutor.run(DefaultBuildOperationExecutor.java:110)
        at org.gradle.execution.taskgraph.DefaultTaskGraphExecuter$EventFiringTaskWorker.execute(DefaultTaskGraphExecuter.java:241)
        at org.gradle.execution.taskgraph.DefaultTaskGraphExecuter$EventFiringTaskWorker.execute(DefaultTaskGraphExecuter.java:230)
        at org.gradle.execution.taskgraph.DefaultTaskPlanExecutor$TaskExecutorWorker.processTask(DefaultTaskPlanExecutor.java:123)
        at org.gradle.execution.taskgraph.DefaultTaskPlanExecutor$TaskExecutorWorker.access$200(DefaultTaskPlanExecutor.java:79)
        at org.gradle.execution.taskgraph.DefaultTaskPlanExecutor$TaskExecutorWorker$1.execute(DefaultTaskPlanExecutor.java:104)
        at org.gradle.execution.taskgraph.DefaultTaskPlanExecutor$TaskExecutorWorker$1.execute(DefaultTaskPlanExecutor.java:98)
        at org.gradle.execution.taskgraph.DefaultTaskExecutionPlan.execute(DefaultTaskExecutionPlan.java:626)
        at org.gradle.execution.taskgraph.DefaultTaskExecutionPlan.executeWithTask(DefaultTaskExecutionPlan.java:581)
        at org.gradle.execution.taskgraph.DefaultTaskPlanExecutor$TaskExecutorWorker.run(DefaultTaskPlanExecutor.java:98)
        at org.gradle.internal.concurrent.ExecutorPolicy$CatchAndRecordFailures.onExecute(ExecutorPolicy.java:63)
        at org.gradle.internal.concurrent.ManagedExecutorImpl$1.run(ManagedExecutorImpl.java:46)
        at org.gradle.internal.concurrent.ThreadFactoryImpl$ManagedThreadRunnable.run(ThreadFactoryImpl.java:55)
Caused by: java.lang.IllegalArgumentException: superClassName is empty!
        at org.gradle.api.internal.tasks.testing.detection.AbstractTestFrameworkDetector.getSuperTestClassFile(AbstractTestFrameworkDetector.java:67)
        at org.gradle.api.internal.tasks.testing.junit.JUnitDetector.processTestClass(JUnitDetector.java:50)
        at org.gradle.api.internal.tasks.testing.detection.AbstractTestFrameworkDetector.processTestClass(AbstractTestFrameworkDetector.java:142)
        at org.gradle.api.internal.tasks.testing.detection.DefaultTestClassScanner$1.visitClassFile(DefaultTestClassScanner.java:57)
        at org.gradle.api.internal.tasks.testing.detection.DefaultTestClassScanner$ClassFileVisitor.visitFile(DefaultTestClassScanner.java:78)
        at org.gradle.api.internal.file.collections.jdk7.Jdk7DirectoryWalker$1.visitFile(Jdk7DirectoryWalker.java:86)
        at org.gradle.api.internal.file.collections.jdk7.Jdk7DirectoryWalker$1.visitFile(Jdk7DirectoryWalker.java:59)
        at org.gradle.api.internal.file.collections.jdk7.Jdk7DirectoryWalker.walkDir(Jdk7DirectoryWalker.java:59)
        at org.gradle.api.internal.file.collections.DirectoryFileTree.walkDir(DirectoryFileTree.java:155)
        at org.gradle.api.internal.file.collections.DirectoryFileTree.visitFrom(DirectoryFileTree.java:133)
        at org.gradle.api.internal.file.collections.DirectoryFileTree.visit(DirectoryFileTree.java:118)
        at org.gradle.api.internal.file.collections.FileTreeAdapter.visit(FileTreeAdapter.java:110)
        at org.gradle.api.internal.file.CompositeFileTree.visit(CompositeFileTree.java:87)
        at org.gradle.api.internal.tasks.testing.detection.DefaultTestClassScanner.detectionScan(DefaultTestClassScanner.java:55)
        at org.gradle.api.internal.tasks.testing.detection.DefaultTestClassScanner.run(DefaultTestClassScanner.java:49)
        at org.gradle.api.internal.tasks.testing.processors.TestMainAction.run(TestMainAction.java:55)
        at org.gradle.api.internal.tasks.testing.detection.DefaultTestExecuter.execute(DefaultTestExecuter.java:104)
        at org.gradle.api.internal.tasks.testing.detection.DefaultTestExecuter.execute(DefaultTestExecuter.java:47)
        at org.gradle.api.tasks.testing.AbstractTestTask.executeTests(AbstractTestTask.java:449)
        at org.gradle.api.tasks.testing.Test.executeTests(Test.java:530)
        at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
        at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
        at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
        at org.gradle.internal.reflect.JavaMethod.invoke(JavaMethod.java:73)
        at org.gradle.api.internal.project.taskfactory.StandardTaskAction.doExecute(StandardTaskAction.java:46)
        at org.gradle.api.internal.project.taskfactory.StandardTaskAction.execute(StandardTaskAction.java:39)
        at org.gradle.api.internal.project.taskfactory.StandardTaskAction.execute(StandardTaskAction.java:26)
        at org.gradle.api.internal.AbstractTask$TaskActionWrapper.execute(AbstractTask.java:780)
        at org.gradle.api.internal.AbstractTask$TaskActionWrapper.execute(AbstractTask.java:747)
        at org.gradle.api.internal.tasks.execution.ExecuteActionsTaskExecuter$1.run(ExecuteActionsTaskExecuter.java:121)
        at org.gradle.internal.progress.DefaultBuildOperationExecutor$RunnableBuildOperationWorker.execute(DefaultBuildOperationExecutor.java:336)
        at org.gradle.internal.progress.DefaultBuildOperationExecutor$RunnableBuildOperationWorker.execute(DefaultBuildOperationExecutor.java:328)
        at org.gradle.internal.progress.DefaultBuildOperationExecutor.execute(DefaultBuildOperationExecutor.java:199)
        at org.gradle.internal.progress.DefaultBuildOperationExecutor.run(DefaultBuildOperationExecutor.java:110)
        at org.gradle.api.internal.tasks.execution.ExecuteActionsTaskExecuter.executeAction(ExecuteActionsTaskExecuter.java:110)
        at org.gradle.api.internal.tasks.execution.ExecuteActionsTaskExecuter.executeActions(ExecuteActionsTaskExecuter.java:92)
        ... 29 more

If I understand correctly JUnitDetector assumes that a test class always has a super class. Technically that should be true. Is it possible this is some sort of module visibility problem?
Do you think I should create an issue in github?

Thanks.

Here’s the corresponding github issue.

It assumes that it can scan all of the classes to find a test based on the rules for the testing framework, and if the current class is not a test, that there is a super class that it can check as well. However, your problem is unrelated to the actual test class. It is caused by module-info.java, which is compiled to module-info.class, but isn’t really a class.

You can configure the test task to exclude this file, and it will not be scanned:

test {
    exclude 'module-info.class'
}

However, that is not the only problem.

You have that problem too, which is more obvious once you correct the first problem. It will then fail with:

java.lang.IllegalAccessException:
  class org.junit.runners.BlockJUnit4ClassRunner (in module junit) cannot access
  class org.foo.test.DummyTest (in module foo.test)
  because module foo.test does not export org.foo.test to module junit

It should work as expected if you add exports org.foo.test; to the module info.

P.S. You might have received a quicker response if you had used GitHub for the sample project. Downloading foo.zip from “Bogus Abuser” doesn’t exactly sound like a safe download.