Cdacians

Cdacians
Cdacians

Thursday, 7 September 2017

Gradle tip #3: Tasks ordering

Gradle tip #3: Tasks ordering


I noticed that the quite often problem I face when I work with Gradle - is tasks ordering (either existing or my custom ones). Apparently my build works better when my tasks are executed at the right moment of the build process :)
So let's dig deeper into how can we change tasks execution order.

dependsOn

I believe the most obvious way of telling your task to execute after some other task - is to use dependsOn method.
Let's consider existing task A and we need to add task B which executes only after task A is executed:
This is probably the easiest thing you can do. Given that tasks A and B are already defined:
task A << {println 'Hello from A'}
task B << {println 'Hello from B'}
What you need to do - is just tell Gradle that task B depends on task A
B.dependsOn A
This means that whenever I try to execute task B - Gradle will take care of executing task A as well:
paveldudka$ gradle B
:A
Hello from A
:B
Hello from B
Alternatively, you could declare such a dependency right inside task configuration section:
task A << {println 'Hello from A'}
task B {
    dependsOn A
    doLast {
        println 'Hello from B'  
    }
}
Result is the same.
But what if we want to insert our task inside already existing task graph?
The process is pretty much the same:
original task graph:
task A << {println 'Hello from A'}
task B << {println 'Hello from B'}
task C << {println 'Hello from C'}

B.dependsOn A
C.dependsOn B
our new custom task:
task B1 << {println 'Hello from B1'}
B1.dependsOn B
C.dependsOn B1
output:
paveldudka$ gradle C
:A
Hello from A
:B
Hello from B
:B1
Hello from B1
:C
Hello from C
Please note, that dependsOn adds task to the set of dependencies. Thus it is totally fine to be dependent on multiple tasks:
task B1 << {println 'Hello from B1'}
B1.dependsOn B
B1.dependsOn Q
output:
paveldudka$ gradle B1
:A
Hello from A
:B
Hello from B
:Q
Hello from Q
:B1
Hello from B1

mustRunAfter

Now imagine that our task depends on 2 other tasks. For this example I decided to use more real-life case. Imagine I have one task for unit tests and another for UI tests. Also I have a task which executes both unit & UI tests:
task unit << {println 'Hello from unit tests'}
task ui << {println 'Hello from UI tests'}
task tests << {println 'Hello from all tests!'}

tests.dependsOn unit
tests.dependsOn ui
output:
paveldudka$ gradle tests
:ui
Hello from UI tests
:unit
Hello from unit tests
:tests
Hello from all tests!
Even though tasks unit and UI tests will be executed before task tests, the order of execution for tasks ui and unit is not determined. Right now I believe they will be executed in alphabetical order, but this behavior is an implementation detail and you definitely should not rely on this fact.
Since UI tests are executing much longer than unit tests, I want my unit tests run first and only if everything OK - proceed to executing UI tests. So what should I do if I want my unit tests run before UI tests?
One way for solving this would be to make UI test task depend on unit test task:
task unit << {println 'Hello from unit tests'}
task ui << {println 'Hello from UI tests'}
task tests << {println 'Hello from all tests!'}

tests.dependsOn unit
tests.dependsOn ui
ui.dependsOn unit // <-- I added this dependency
output
paveldudka$ gradle tests
:unit
Hello from unit tests
:ui
Hello from UI tests
:tests
Hello from all tests!
Now my unit tests are getting executed before UI tests! Great!
BUT! There is one really big fat nasty problem with this approach! My UI tests do not really depend on unit tests. I wanna be able to run my UI tests separately, but now every time I want to run my UI tests - my unit tests will be run as well!
That's where mustRunAfter method comes into play. It tells Gradle to run task aftertask specified as an argument. So essentially, we do not introduce dependency between our unit tests and UI tests, but instead we told Gradle to give unit tests priority if they are executed together, so unit tests are executed before our UI test suite:
task unit << {println 'Hello from unit tests'}
task ui << {println 'Hello from UI tests'}
task tests << {println 'Hello from all tests!'}

tests.dependsOn unit
tests.dependsOn ui
ui.mustRunAfter unit
output
paveldudka$ gradle tests
:unit
Hello from unit tests
:ui
Hello from UI tests
:tests
Hello from all tests!
And the dependency graph looks like:
Notice that we lost explicit dependency between UI tests and unit tests! Now if I decide to run just UI tests - my unit tests won't be executed.
Please note that mustRunAfter is marked as "incubating" (as of Gradle 2.4) which means that this is an experimental feature and its behavior can be changed in future releases.

finalizedBy

Now I have task which runs both UI and unit tests. Great! Let's say each of them produces test report. So I decided to create a task which merges 2 test reports into one:
task unit << {println 'Hello from unit tests'}
task ui << {println 'Hello from UI tests'}
task tests << {println 'Hello from all tests!'}
task mergeReports << {println 'Merging test reports'}

tests.dependsOn unit
tests.dependsOn ui
ui.mustRunAfter unit
mergeReports.dependsOn tests
Now if I want to get test report with both UI & unit tests - I execute mergeReports task:
paveldudka$ gradle mergeReports
:unit
Hello from unit tests
:ui
Hello from UI tests
:tests
Hello from all tests!
:mergeReports
Merging test reports
It works, but... it looks sloppy.. mergeReports task doesn't make a lot of sense from user (by user I mean developer :) ) perspective. I want to be able to execute tests and get merged report. Obviously, I could add merge logic inside tests task, but for the sake of this demo - I want to keep this logic in separate mergeReports task.
finalizedBy method come to the rescue. Its name is quite self-explanatory - it adds finalizer task to this task.
So let's modify our script as follows:
task unit << {println 'Hello from unit tests'}
task ui << {println 'Hello from UI tests'}
task tests << {println 'Hello from all tests!'}
task mergeReports << {println 'Merging test reports'}

tests.dependsOn unit
tests.dependsOn ui
ui.mustRunAfter unit
mergeReports.dependsOn tests

tests.finalizedBy mergeReports
Now I'm able to execute tests task and I still get my merged test report:
paveldudka$ gradle tests
:unit
Hello from unit tests
:ui
Hello from UI tests
:tests
Hello from all tests!
:mergeReports
Merging test reports
Please note that finalizedBy is marked as "incubating" (as of Gradle 2.4) which means that this is an experimental feature and its behavior can be changed in future releases.
This is pretty much it - with these 3 tools you can easily tune your build process!
Happy gradling!

No comments:

Post a Comment