Custom Task and dryRun in gradle 8x

Hey, I’m trying to migrate to gradle 8x from 7x.
There’s already an task something like this

class EfTestTask extends Test {
    @Input
    boolean integration;
 ...
}

now when I’ve upgraded to 8x I have following error:

Could not compile script 'EfTestTask.gradle'.
> startup failed:
  script 'EfTestTask.gradle': 5: Can't have an abstract method in a non-abstract class. The class 'EfTestTask' must be declared abstract or the method 'org.gradle.api.provider.Property getDryRun()' must be implemented.
   @ line 5, column 1.
     class EfTestTask extends Test {
     ^

how can I configure that dryRun? I’ve tried to add

@Input Property<Boolean> dryRun

but then I have this

   > Could not create task of type 'EfTestTask'.
      > Cannot invoke "org.gradle.api.provider.Property.convention(Object)" because the return value of "org.gradle.api.tasks.testing.Test.getDryRun()" is null

this build is somehow complicated and I’m not author of it so I’m trying to understand what should I do in order to migrate to gradle 8x

The “problem” is, that you are extending the built-in Test task class.
Most often extending built-in task classes is not really a good idea.
Anyway, in Gradle 8 the built-in Test task class became abstract and got the abstract getDryRun method.
Just declare your class abstract too and you are fine.

Well, the problem here is that the Test class calls an abstract method at object construction time, it is quite an antipattern.

My Workaround:

open class TestOnSpecificJvmVersion @Inject constructor(jvmVersion: Int) : Test() {

    private lateinit var isADryRun: Property<Boolean>

// Code

    @Suppress("UnstableApiUsage")
    override fun getDryRun(): Property<Boolean> = when {
        ::isADryRun.isInitialized -> isADryRun
        else -> {
            isADryRun = project.objects.property<Boolean>()
            isADryRun
        }
    }
}

Well, the problem here is that the Test class calls an abstract method at object construction time, it is quite an antipattern.

No, that is not the problem.
Even if noone in the world would call that method, you would have the same problem.
Compilation fails, because you are extending a class that is now abstract and has an abstract method.
To extend from an abstract class you either need to implement all abstract methods, or declare your own class abstract too.

Calling abstract methods in constructors usually is an anti-pattern, yes.
But Gradle domain objects that you let Gradle instantiate are special, as Gradle decorates them to implement various things for you, removing the need for quite some boilerplate, and these things you can safely call in the constructor and that is also quite a common pattern in Gradle build logic.

As I said, the anti-pattern here is, that you actually subclass the Test task.
And if you really need to do it, the easiest way to solve the problem is to simply make your own task class abstract too, as Gradle will at runtime properly decoarate the class to implement that method as needed.

If you are really allergic to declaring your class abstract, you should instead of this lateinit hackery just do it the proper way. Let Gradle inject an instance of ObjectFactory into your constructor and directly initialize your dry run field.~

But really, just make your class abstract as I advised above already and let Gradle do its magic. :wink:

Well, I disagree :smile:
In my case, I am developing a plugin that creates multiple tasks to test with multiple Jvms. I need to create my own Test class, and I cannot declare it abstract, or I won’t be able to build it (I tried to see whether the magic works, but with project.tasks.create<TestOnSpecificJvmVersion>("testWithJvm$version", version) it doesn’t (at least inside the plugin).
I am forced to that horrible workaround.

In Test, in the constructor, there’s a call to getDryRun().convention(false); that maybe has very deep design reasons, but to me looks quite suboptimal.

My alternative would be to create tasks of type Test and configure them without any extension to Test. Doable, but extending Test seemed more natural (it is a test task). If some classes are not meant to be extended, maybe a dedicated annotation might help.

I need to create my own Test class

Well, if it really is-a Test class, sure, why not.
As long as you for example also find with things like tasks.withType<Test>().configureEach { ... } matching these instances and so on, it might be appropriate to extend the Tests task.
I just said, that most cases that I have seen where someone extended a task, they should not have done it.
There can still be valid use-cases of course.

and I cannot declare it abstract

Sure you can, why should you not?

or I won’t be able to build it (I tried to see whether the magic works, but with project.tasks.create<TestOnSpecificJvmVersion>("testWithJvm$version", version) it doesn’t (at least inside the plugin).

Why should you not be able to?
This works perfectly fine:

abstract class TestOnSpecificJvmVersion @Inject constructor(jvmVersion: Int) : Test()
val version = 1
println(tasks.create<TestOnSpecificJvmVersion>("testWithJvm$version", version))

Whether or not in a plugin should not make any difference.
If it does “not work” for you, please specify how it does “not work”, or optimally provide an MCVE that shows what does not work.

I am forced to that horrible workaround.

No, you are not.
Just declare the class abstract like everyone else that develops tasks and wants to save boilerplate. :wink:
My other suggestion indeed does not work, due the constructor calling the getter which then of course is too late with doing a field initializer.

but to me looks quite suboptimal

And yet is a quite common pattern when working with domain objects for Gradle build logic. :slight_smile:
Actually usually and idiomatically tasks and extensions should be opinionless and the opinion only added by the plugin creating those extensions and tasks, but if there is opinon / defaults in a domain object, then this pattern is quite common, even if bad practice in normal code.

1 Like

Btw., you should avoid using tasks.create to leverage task-configuration avoidance properly.

1 Like

I’m not entirely sure at this point what the culprit was. Still, the build crashed when the plugin was applied complaining about the impossibility of instancing the Test superclass from the constructor call in my extended class.

I removed it to provide an MCVE and the asbtract task indeed works with Gradle 8.9.
I also applied your suggestion to switch to configuration avoidance, so also thanks for that. I seldom go change my plugins unless I encounter issues with them, so probably the previous implementation was from a time when configuration avoidance was uncommon.

Anyway, in my case it is-a Test, and I wanted to configure it with tasks.withType<Test>().configureEach { ... }, so I guess I hit one of the use cases you mentioned.

1 Like