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:
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.
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.
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…
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:
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?
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:
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).
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.
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.