Search code examples
gradlebuild.gradlegradle-kotlin-dsl

Run task only if invoked from command line with dependency on another task even if it fails


I'll explain my use case first and then the generalized description of what I want to do.

Use case: Open test results

The default $gradle test will run the test task and run all unit tests. I would like to add a browseTest task which will, if specified on the command line, open the test report in the default browser. I already have the code to open the test result, but I need to figure out how to execute it. Here is how I think it should work:

Command Tests up-to-date Run tests? Test result? Open test report?
test no yes success/failure no
test yes no success/failure no
browseTest no yes success/failure yes
browseTest yes no success/failure yes

General case

I want to be able to have one task (i.e. browseTest) add another task (e.g. task) to the task graph without depending on the other task succeeding or failing. If I use dependsOn then the first take failing prevents the second task from executing. Using mustRunAfter specifies an ordering but doesn't add the task to the task graph (and thus will not be executed).

If there was something akin to the following, I believe it would get me what I want:

task("browseTest") {
    addsToTaskGraph("test")
    mustRunAfter("test")
    doLast {
        // Open test results in browser
    }
}

Solution

  • Updated Answer

    I answered my own question again. I was able to successfully create a runsAfter extension function. I would love to be able to clean up the logic for triggering the callback/hook, but overall this is very user-friendly and not overly ineffecient.

    val browseTest = task("browseTest") {
        runsAfter(tasks.test.name) {
            // perform "callback"
            val file = project.file("build/reports/tests/test/index.html")
            browse(file.absolutePath)
        }
    }
    
    fun Task.runsAfter(vararg paths: String, action: () -> Unit) {
        val parent = this
        val helper = task("${name}__runsAfter") {
            doLast {
                if (gradle.taskGraph.hasTask(parent)) {
                    action()
                }
            }
        }
        paths.forEach {
            dependsOn(it)
            tasks[it].finalizedBy(helper)
        }
    }
    

    Original Answer

    I was able to get this working, although I don't really like the result. If anyone has any other ideas, please let me know.

    val browseTest = task("browseTest") {
        dependsOn("test")
        val parent = this
        val helper = task("browseTestHelper") {
            doLast {
                if (gradle.taskGraph.hasTask(parent)) {
                    // Open test results in browser
                    val file = project.file("build/reports/tests/test/index.html")
                    browse(file.absolutePath)
                }
            }
        }
        tasks.test {
            finalizedBy(helper)
        }
    }
    

    There are a few pieces here:

    1. The browseTest task depends on test, which means that it will force test to run if it is specified.
    2. The browseTest task doesn't actually do anything.
    3. A browseTestHelper task is created.
    4. The test task is configured to be "finalized" by the browseTestHelper.
    5. When the browseTestHelper runs, it checks to see if the original browseTest task is in the task graph. If not, it doesn't do anything.
      • This is necessary because the browseTestHelper task is created and finalizes test even if the browseTest task is not invoked.
    6. If browseTest is in the task graph, it performs its actions.

    There are a few ways this could improve:

    1. Is there a better way to access the "parent" task other than the val parent = this line?
    2. Can you conditionally add finalizers to a task while also seeing if a task is present in the task graph?
      • I'm guessing not, since the act of adding a finalizer affects the task graph.
    3. Is there a way to wrap this all in a function that you could invoke with just a function call?

    It would be great to do something like this:

    val browseTest = task("browseTest") {
        runsAfter("test") {
            val file = project.file("build/reports/tests/test/index.html")
            browse(file.absolutePath)
        }
    }