Can't apply JaCoCo extension to custom task

Attempting to apply the ‘jacoco’ extension to a custom task (as shown in the tests of the jacoco plugin) results in an exception. Given the following trivial build file

apply plugin: 'java'
sourceSets {
    foo.java.srcDirs file('src/foo/java')
}
  task fooTest(type: Test, dependsOn: jar) {
    testClassesDir = sourceSets.foo.output.classesDir
    classpath = sourceSets.foo.runtimeClasspath
    reports.html.enabled = false
}
  fooTest.mustRunAfter test
  apply plugin: 'jacoco'
  jacoco {
    toolVersion = '0.7.2.201409121644'
}
  project.jacoco.applyTo(fooTest)

a build error occurs

* Where:
Build file '/private/tmp/build.gradle' line: 20
  * What went wrong:
A problem occurred evaluating root project 'tmp'.
> Cannot add extension with name 'jacoco', as there is an extension already registered with that name.
  * Try:
Run with --info or --debug option to get more log output.
  * Exception is:
org.gradle.api.GradleScriptException: A problem occurred evaluating root project 'tmp'.
 at org.gradle.groovy.scripts.internal.DefaultScriptRunnerFactory$ScriptRunnerImpl.run(DefaultScriptRunnerFactory.java:54)
 at org.gradle.configuration.DefaultScriptPluginFactory$ScriptPluginImpl.apply(DefaultScriptPluginFactory.java:190)
 at org.gradle.configuration.project.BuildScriptProcessor.execute(BuildScriptProcessor.java:39)
 at org.gradle.configuration.project.BuildScriptProcessor.execute(BuildScriptProcessor.java:26)
 at org.gradle.configuration.project.ConfigureActionsProjectEvaluator.evaluate(ConfigureActionsProjectEvaluator.java:34)
 at org.gradle.configuration.project.LifecycleProjectEvaluator.evaluate(LifecycleProjectEvaluator.java:55)
 at org.gradle.api.internal.project.AbstractProject.evaluate(AbstractProject.java:521)
 at org.gradle.api.internal.project.AbstractProject.evaluate(AbstractProject.java:82)
 at org.gradle.configuration.DefaultBuildConfigurer.configure(DefaultBuildConfigurer.java:31)
 at org.gradle.initialization.DefaultGradleLauncher.doBuildStages(DefaultGradleLauncher.java:129)
 at org.gradle.initialization.DefaultGradleLauncher.doBuild(DefaultGradleLauncher.java:106)
 at org.gradle.initialization.DefaultGradleLauncher.run(DefaultGradleLauncher.java:86)
 at org.gradle.launcher.exec.InProcessBuildActionExecuter$DefaultBuildController.run(InProcessBuildActionExecuter.java:80)
 at org.gradle.launcher.cli.ExecuteBuildAction.run(ExecuteBuildAction.java:33)
 at org.gradle.launcher.cli.ExecuteBuildAction.run(ExecuteBuildAction.java:24)
 at org.gradle.launcher.exec.InProcessBuildActionExecuter.execute(InProcessBuildActionExecuter.java:36)
 at org.gradle.launcher.exec.InProcessBuildActionExecuter.execute(InProcessBuildActionExecuter.java:26)
 at org.gradle.launcher.daemon.server.exec.ExecuteBuild.doBuild(ExecuteBuild.java:47)
 at org.gradle.launcher.daemon.server.exec.BuildCommandOnly.execute(BuildCommandOnly.java:34)
 at org.gradle.launcher.daemon.server.exec.DaemonCommandExecution.proceed(DaemonCommandExecution.java:125)
 at org.gradle.launcher.daemon.server.exec.WatchForDisconnection.execute(WatchForDisconnection.java:35)
 at org.gradle.launcher.daemon.server.exec.DaemonCommandExecution.proceed(DaemonCommandExecution.java:125)
 at org.gradle.launcher.daemon.server.exec.ResetDeprecationLogger.execute(ResetDeprecationLogger.java:24)
 at org.gradle.launcher.daemon.server.exec.DaemonCommandExecution.proceed(DaemonCommandExecution.java:125)
 at org.gradle.launcher.daemon.server.exec.StartStopIfBuildAndStop.execute(StartStopIfBuildAndStop.java:33)
 at org.gradle.launcher.daemon.server.exec.DaemonCommandExecution.proceed(DaemonCommandExecution.java:125)
 at org.gradle.launcher.daemon.server.exec.ReturnResult.execute(ReturnResult.java:34)
 at org.gradle.launcher.daemon.server.exec.DaemonCommandExecution.proceed(DaemonCommandExecution.java:125)
 at org.gradle.launcher.daemon.server.exec.ForwardClientInput$2.call(ForwardClientInput.java:71)
 at org.gradle.launcher.daemon.server.exec.ForwardClientInput$2.call(ForwardClientInput.java:69)
 at org.gradle.util.Swapper.swap(Swapper.java:38)
 at org.gradle.launcher.daemon.server.exec.ForwardClientInput.execute(ForwardClientInput.java:69)
 at org.gradle.launcher.daemon.server.exec.DaemonCommandExecution.proceed(DaemonCommandExecution.java:125)
 at org.gradle.launcher.daemon.server.exec.LogToClient.doBuild(LogToClient.java:60)
 at org.gradle.launcher.daemon.server.exec.BuildCommandOnly.execute(BuildCommandOnly.java:34)
 at org.gradle.launcher.daemon.server.exec.DaemonCommandExecution.proceed(DaemonCommandExecution.java:125)
 at org.gradle.launcher.daemon.server.exec.EstablishBuildEnvironment.doBuild(EstablishBuildEnvironment.java:70)
 at org.gradle.launcher.daemon.server.exec.BuildCommandOnly.execute(BuildCommandOnly.java:34)
 at org.gradle.launcher.daemon.server.exec.DaemonCommandExecution.proceed(DaemonCommandExecution.java:125)
 at org.gradle.launcher.daemon.server.exec.StartBuildOrRespondWithBusy$1.run(StartBuildOrRespondWithBusy.java:45)
 at org.gradle.launcher.daemon.server.DaemonStateCoordinator.runCommand(DaemonStateCoordinator.java:258)
 at org.gradle.launcher.daemon.server.exec.StartBuildOrRespondWithBusy.doBuild(StartBuildOrRespondWithBusy.java:49)
 at org.gradle.launcher.daemon.server.exec.BuildCommandOnly.execute(BuildCommandOnly.java:34)
 at org.gradle.launcher.daemon.server.exec.DaemonCommandExecution.proceed(DaemonCommandExecution.java:125)
 at org.gradle.launcher.daemon.server.exec.HandleCancel.execute(HandleCancel.java:36)
 at org.gradle.launcher.daemon.server.exec.DaemonCommandExecution.proceed(DaemonCommandExecution.java:125)
 at org.gradle.launcher.daemon.server.exec.HandleStop.execute(HandleStop.java:30)
 at org.gradle.launcher.daemon.server.exec.DaemonCommandExecution.proceed(DaemonCommandExecution.java:125)
 at org.gradle.launcher.daemon.server.exec.DaemonHygieneAction.execute(DaemonHygieneAction.java:39)
 at org.gradle.launcher.daemon.server.exec.DaemonCommandExecution.proceed(DaemonCommandExecution.java:125)
 at org.gradle.launcher.daemon.server.exec.CatchAndForwardDaemonFailure.execute(CatchAndForwardDaemonFailure.java:32)
 at org.gradle.launcher.daemon.server.exec.DaemonCommandExecution.proceed(DaemonCommandExecution.java:125)
 at org.gradle.launcher.daemon.server.exec.DefaultDaemonCommandExecuter.executeCommand(DefaultDaemonCommandExecuter.java:52)
 at org.gradle.launcher.daemon.server.DefaultIncomingConnectionHandler$ConnectionWorker.handleCommand(DefaultIncomingConnectionHandler.java:154)
 at org.gradle.launcher.daemon.server.DefaultIncomingConnectionHandler$ConnectionWorker.receiveAndHandleCommand(DefaultIncomingConnectionHandler.java:128)
 at org.gradle.launcher.daemon.server.DefaultIncomingConnectionHandler$ConnectionWorker.run(DefaultIncomingConnectionHandler.java:116)
 at org.gradle.internal.concurrent.DefaultExecutorFactory$StoppableExecutorImpl$1.run(DefaultExecutorFactory.java:64)
Caused by: java.lang.IllegalArgumentException: Cannot add extension with name 'jacoco', as there is an extension already registered with that name.
 at org.gradle.api.internal.plugins.ExtensionsStorage.add(ExtensionsStorage.java:38)
 at org.gradle.api.internal.plugins.DefaultConvention.add(DefaultConvention.java:101)
 at org.gradle.api.internal.plugins.DefaultConvention.create(DefaultConvention.java:107)
 at org.gradle.testing.jacoco.plugins.JacocoPluginExtension.applyTo(JacocoPluginExtension.groovy:68)
 at org.gradle.testing.jacoco.plugins.JacocoPluginExtension$applyTo.call(Unknown Source)
 at build_1693aa2hrm7cg27hn9if4lk3hm.run(/private/tmp/build.gradle:20)
 at org.gradle.groovy.scripts.internal.DefaultScriptRunnerFactory$ScriptRunnerImpl.run(DefaultScriptRunnerFactory.java:52)
 ... 56 more

due to https://github.com/gradle/gradle/blob/master/subprojects/jacoco/src/main/groovy/org/gradle/testing/jacoco/plugins/JacocoPluginExtension.groovy#L68

JacocoTaskExtension extension = task.extensions.create(TASK_EXTENSION_NAME, JacocoTaskExtension, agent, task)

The fix would be to create the extension only if it doesn’t exist already.

BTW, there’s no test covering this use case, that is, apply the ‘jacoco’ extension to a custom task after the default jacoco customizations have been applied to the default Test task provided by the Java plugin.

IOW, invoke ‘project.jacoco.applyTo(task)’ twice.

The ‘jacoco’ task extension is automatically applied to all tasks of type ‘Test’. Removing ‘project.jacoco.applyTo(fooTest)’ should solve your problem.

Actually no, it’s not, which is why I explicitly call ‘jacoco.applyTo’. According to what I can read in the code a new task named ‘jacocoFooTestReport’ should be added automatically, sadly that’s not the case.

Running ‘gradle tasks --all’ without the explicit ‘jacoco.applyTo’ leads to

Other tasks
-----------
fooTest [fooClasses, jar]
jacocoTestReport [classes]

Any ideas? Perhaps I’m missing something obvious here.

If you want a separate report, you need to add your own ‘JacocoReport’ task (otherwise use ‘jacocoTestReport.executionData fooTest’). You don’t need ‘jacoco.applyTo(fooTest)’, because that’s automatically done for all tasks of type ‘Test’.

I thought that task was supposed to be added automatically given https://github.com/gradle/gradle/blob/master/subprojects/jacoco/src/main/groovy/org/gradle/testing/jacoco/plugins/JacocoPlugin.groovy#L158

‘if (task.name == JavaPlugin.TEST_TASK_NAME) { … }’ means that it only gets added for the ‘test’ task, not for every task of type ‘Test’. Probably the idea is that it’s up to you whether you want a single or multiple reports.