Help with task ordering?

Looking for suggestions to get around a block. The (simplified) scenario is that I have a “war” project, which depends on some other “java-library” projects containing the modules of actual functionality. I have integration tests (Selenium) in the library projects (along side the code they test), but those tests cannot actually test the libraries in isolation - they require them to be packaged into the “war” and for that “war” to be running in a server.

I have all the independent pieces working — but the bit that’s eluding me is gluing them together in such a way that the developer can run a single task to make things happen. What I need is to run three specific tasks in exactly that order:

  1. :war:startServer
  2. :library1:runSeleniumTests (or maybe library2, library3, etc)
  3. :war:stopServer

The obvious answer would be to have runSeleniumTests depend on startServer and be finalised by stopServer… but having library1 depend on the downstream project isn’t workable.

Having an extra task in “war” which depends on startServer and runSeleniumTests and is finalised by stopServer would work, except that I see no way to control the ordering of startServer and runSeleniumTests in that scenario.

Having that task explicitly run the tests as its own action (instead of as a dependent task) is also a possibility, but I haven’t figured out how to do it yet.

Any useful suggestions?

Actually for things like starting and stopping a server a test task needs, usually a shared build service is the way to go. You might look into that.


Besides that,

but having library1 depend on the downstream project isn’t workable

why not?

except that I see no way to control the ordering of startServer and runSeleniumTests in that scenario

runSeleniumTests.mustRunAfter(startServer), or, well, runSeleniumTests.dependsOn(startServer)

Well, right now, they’re actually in separate included builds, following the example on structuring large projects (and this is a large project… there are actually about a dozen of the library projects and three war files, and a tonne of other stuff besides). That’s not necessarily a hard requirement though… just how I’ve been trying to structure things during conversion to Gradle (the existing code is an unholy tangle of Ant scripts).

But maybe I have some bad assumptions in there. I’ve been strict about avoiding any hint of cyclic dependencies, but do you not see any concerns with having two-way dependencies between projects, provided that the tasks themselves do not form cycles? Running library1:runSelenium depending on war:startServer which depends on library1’s jar artifact being built? There’s no loop, but I had assumed that bidirectional references between projects was generally a bad thing…

Oh, and the build service does look like something I should look more closely at. I’ve seen the term used before, but hadn’t realised it was quite so applicable to some of the things I’m trying to do… it might allow me to remove - or at least, improve - some of the custom code I’ve been using to get this working.

but do you not see any concerns with having two-way dependencies between projects, provided that the tasks themselves do not form cycles?

Exactly. As long as there are no real cycles, two-way dependencies between projects are not that bad I’d say.

Ok, I’ll give that a try on Monday. I’ve already merged most of the included builds, since I think the degree of separation was creating complexity for no real benefit, so the library projects should be able to see the war projects and their tasks now…

1 Like

Trying this out, I’m running into what I think is a problem with the order in which the projects are configured? If I add something like this to war-project:

println project(':library1').tasks.getByName('runSeleniumTests')

…then it works, printing out the task instance. But if I do the reverse, from library1:

println project(':war').tasks.getByName('startServer')

Then I get an UnknownTaskException… and do so on any task I try, not just that one. I assume this is because the tasks in war don’t exist yet because we haven’t gotten around to configuring that project yet?

Just depend on ':war:startServer'. (yes, the string) :slight_smile:

Ok, looking for a sanity check. I’ve ended up with the following three chunks of code.

First, in the plugin where I’m also registering the Selenium test suites for the library projects:

interface SeleniumExtension {
    Property<String> getWarProject()
}
def seleniumExtension = project.extensions.create("selenium", SeleniumExtension);

project.afterEvaluate {
    String warProject = seleniumExtension.warProject.get()
    project.tasks.selenium.mustRunAfter "${warProject}:startServer"
}

Second, in the various library projects. I could have just hardcoded the dependency in each project, but we have enough projects that I want to enforce consistency better:

selenium {
    warProject = 'war-1'
}

…and finally in the war projects:

tasks.register('runSelenium') {
    dependsOn tasks.startServer
    dependsOn ':library1:selenium'
    dependsOn ':library2:selenium'
    finalizedBy tasks.stopServer
}

It seems to work (though needs some work around test reporting), but can you confirm that I’m not doing anything particularly horrific from a Gradle point of view?

can you confirm that I’m not doing anything particularly horrific from a Gradle point of view

Unfortunately not, practically any afterEvaluate is horrific.
If you want to do it through such an extension, then make the extension have a method that adds the mustRunAfter.

Besides that, a shared build service might still be better suited.
Because now even if both selenium tasks are up-to-date, the server is started and stopped.
If you do it properly using a shared build service, it only needs to be started and stopped if it really needs to.

Something similar to this:

abstract class Producer : DefaultTask() {
    @get:OutputFile
    abstract val output: RegularFileProperty

    @TaskAction
    fun produce() {
        output.get().asFile.writeText("FOO")
    }
}

val producer by tasks.registering(Producer::class) {
    output.set(layout.buildDirectory.file("foo"))
}

abstract class DeployingService : BuildService<DeployingService.Params> {
    interface Params : BuildServiceParameters {
        val deployable: RegularFileProperty
    }

    @get:InputFile
    val deployable = parameters.deployable
}

abstract class IntegTest : DefaultTask() {
    @get:Nested
    abstract val deployingService: Property<DeployingService>

    @TaskAction
    fun test() {
        println(deployingService.get().deployable.get().asFile.readText())
    }
}

val deployingServiceProvider = gradle.sharedServices.registerIfAbsent("deployingService", DeployingService::class) {
    parameters.deployable.set(producer.flatMap { it.output })
}

val integTest by tasks.registering(IntegTest::class) {
    // work-around for https://github.com/gradle/gradle/issues/24512
    dependsOn(producer)
    usesService(deployingServiceProvider)
    deployingService.set(deployingServiceProvider)
}

Unfortunately not, practically any afterEvaluate is horrific.

Exactly the kind of feedback I was looking for.

I picked that up from some random StackOverflow suggestions after finding that my extension properties were all null — presumably because I was trying to use them too early, before the projects had had a chance to set them. But changing the “warProject” property into a method works fine too… and I’ve ended up changing it to dependsOn (and adding finalizedBy to shut the server down), since in hindsight I don’t really need to decouple them…

Besides that, a shared build service might still be better suited.

I’m taking a look at that option… it’s not working yet, but that’s mostly because I headed down a false track of trying to run the server in-process in Gradle, but forgot that I need it as a separate process (it pokes around in some Java settings in a way that doesn’t play nicely with others).

// work-around for Implicit task dependencies for shared build services · Issue #24512 · gradle/gradle · GitHub

So this is because there’s no way of tying the build service itself to the output of the producer task (in my case, the “war”) — so the workaround is to ensure that all tasks which use the service have the dependency instead?

Then in my case, the obvious place to hack this is inside the SeleniumExtension class, where I’m currently doing the dependsOn part… set the service in the same place. Not sure how to inject the server URL into a JUnit task, but I’ll figure that out.

1 Like

Ok, it’s taken a couple of days, but I’ve got a build service up and running, seems to be working pretty well. I do have some other things to ask about, but they’re a little off topic for this post, so I’ll ask them separately.

Thanks for your help on this one…

1 Like