Show stacktraces only for unexpected exceptions in Gradle - gradle

In my build files I always set option to show stacktraces by default
gradle.startParameter.showStacktrace = org.gradle.api.logging.configuration.ShowStacktrace.ALWAYS
This is very useful when unexpected failure happens. However, there are some types of failures and causes that are typical (like compilation failure or code quality violation) and in such cases I don't want stacktrace - just meaningful output on console is enough.
Is there a way to disable showing stacktrace for some certain whitelist of exceptions or tasks?

Here is one way, though it is somewhat involved. The idea is based on the notion of a Custom Logger.
Imagine that we have a Java project with 2 extra tasks:
apply plugin: 'java'
task foo() << {
throw new IllegalStateException("from foo")
}
task bar() << {
throw new GradleException("from bar")
}
Suppose further that we want to suppress stacktraces (or print a condensed version) for IllegalStateException and CompilationFailedException (but not GradleException). To this end, consider the following init.gradle file:
useLogger(new CustomEventLogger())
class CustomEventLogger extends BuildAdapter implements TaskExecutionListener {
def exceptionWhitelist = ["IllegalStateException", "CompilationFailedException"]
public void beforeExecute(Task task) {
println "[$task.name]"
}
public void afterExecute(Task task, TaskState state) {
println()
}
private def getCauses(def throwable) {
def causes = []
def t = throwable
while (t != null) {
causes << t.class.simpleName
t = t.cause
}
causes
}
public void buildFinished(BuildResult result) {
println 'build completed'
def throwable = result.failure
if (throwable != null) {
println "TRACER failed with: " + throwable.class
def causes = getCauses(throwable)
if (!hasCauseInWhitelist(causes)) {
throwable.printStackTrace()
} else {
causes.each { println it }
}
}
}
}
The init.gradle must be specified on the command-line. e.g. gradle -I init.gradle compileJava.
It will print condensed info for the two exceptions specified. (Note that walks the hierarchy of causes to find a match.)
The downside is that this "completely disables Gradle's default output", so there may be further tweaks required, depending on requirements.
For example, if I intentionally put a syntax error in Java:
bash$ gradle -I init.gradle compileJava
[compileJava]
~/src/main/java/net/codetojoy/Employee.java:4: error: class, interface, or enum expected
public clazz Employee {
^
1 error
build completed
TRACER failed with: class org.gradle.internal.exceptions.LocationAwareException
LocationAwareException
TaskExecutionException
CompilationFailedException
then we observe the condensed info for the stacktrace.

Related

How to get Gradle to fail tests on certain console output

I am trying to configure Gradle to fail a test upon detecting a particular console output, but only if that test did not already fail.
For better context, I am trying to fail a test whenever the word "LEAK" is logged to the console, which Netty will automatically log.
My current solution looks like this:
ext {
testsWithLeak = [:]
}
test {
useJUnitPlatform()
onOutput { test, output ->
if (output.message.contains("LEAK")) {
testsWithLeak[test] = output.message
}
}
afterTest { test, result ->
if (result.getResultType().equals(TestResult.ResultType.FAILURE)) {
testsWithLeak.remove(test);
}
}
finalizedBy 'checkLeaks'
}
task checkLeaks {
dependsOn 'test'
doLast {
testsWithLeak.each{entry ->
throw new GradleException("ERROR: $entry.key produced leak: $entry.value")
}
}
}
This mostly works, and will correctly fail a build upon detecting a "LEAK" message. The problem is that since the test itself is considered a SUCCESS, it appears to be cached and not run again of subsequent builds. I tried calling cleanTest as part of throwing a GradleException, but that did not help.
Note that I use the testsWithLeak variable because I only want to throw this exception if the test did not already fail. A test may fail due to other assertions in which case I do not want to check for leaks (as the GradleException may mask the underlying assertion failure).
Also note that throwing the GradleException as part of the afterTest closure does not appear to fail the build.
How can I:
Fail a build when a test logs this message
Only throw this exception when the test did not otherwise fail
Ensure the build will correctly fail on subsequent attempts (without relying on commandline options)
The test is considered up to date because the test inputs (the class files) and the test outputs (the xml report) have not changed since the previous run. You can do the following so that tests are never up to date
test {
outputs.upToDateWhen {false}
...
}
See
https://docs.gradle.org/current/javadoc/org/gradle/api/tasks/TaskInputs.html
https://docs.gradle.org/current/javadoc/org/gradle/api/tasks/TaskOutputs.html
Your testsWithLeak map will not persist between gradle invocations. I suggest you write to a file under $buildDir and add a TaskOutputs.upToDateWhen {...} predicate to check the file exists. Your checkLeaks tasks should also be driven by the file and not a project variable.
Eg:
test {
ext {
testsWithLeak = new java.util.Properties()
leakFile = file("$buildDir/testsWithLeak.props")
}
outputs.upToDateWhen {!leakFile.exists()}
onOutput {...}
afterTest {...}
doFirst {
delete leakFile
}
doLast {
if (testsWithLeak) {
leakFile.withWriter { testsWithLeak.store(it)}
}
}
}

Execute gradle task after test

After executing test task in Gradle build I want to always execute additional task which should have access to test result. Something like:
task('afterTest') {
doLast {
if (testSuccessful) {
//
} else {
//
}
}
}
There are two parts in your question: 1) make a custom task executed always after test task is executed, 2) make the test "result" available in this custom task.
1) First part is very simple, you just have to use the dedicated Task.finalizedBy method to create a "finalized by" dependency between test task and your custom task. (see Finalizer tasks)
2) Second part is a bit tricky, as there is no simple way provided by Gradle, as far as I know, to get the "result" (SUCCESS or FAILURE) of test task. But you could use API exposed by the Test Task to store the number of test in error into a variable and test this counter in your custom task : here is a working example:
ext{
// counter of test cases in error
nbTestOnError = 0
}
test {
// use "afterTest" hook to increment nbTestOnError counter
afterTest { desc , result ->
if (result.getResultType().equals(TestResult.ResultType.FAILURE)){
nbTestOnError++
}
}
}
task('afterTest') {
doLast {
// implement your logic depending on counter value
if (nbTestOnError > 0) {
// do something if test failed
} else{
// do something when all tests passed
}
}
}
// create "finalized by" dependency
test.finalizedBy afterTest
EDIT : based on important remark in comment from #lance-java : in order to support up-to-date check , you could configure your custom task to be "skipped" if test task is not executed. A simple way would be to use Task upToDateWhen feature (see here) :
task('afterTest') {
// consider this task "UP TO DATE" if `test` task did not execute
outputs.upToDateWhen {
!test.didWork
}
doLast {
//...
}
}
As I said in the other thread, it's best to use the file system to pass values from one task to another. This way the value will be available even if the test task was up to date and skipped. Eg:
test {
outputs.file "$buildDir/test.properties"
ext.errorCount = 0
afterTest { desc , result ->
if (result.resultType.name() == "FAILURE") {
errorCount++
}
}
doLast {
file("$buildDir/test.properties").text = "errorCount=$errorCount"
}
finalizedBy 'afterTest'
ignoreFailures = true
}
task afterTest {
dependsOn 'test'
inputs.file "$buildDir/test.properties"
doLast {
def props = new Properties()
props.load(file("$buildDir/test.properties"))
def errorCount = Integer.parseInt(props['errorCount'])
if (errorCount) {
// doStuff
throw new TaskExecutionException("$errorCount tests failed")
}
}
}
Building on the existing answers, use org.gradle.api.tasks.testing.Test.afterSuite to save the test result to a file:
test {
afterSuite { descriptor, result ->
if (descriptor.parent == null) {
file("$buildDir/test.result").text = result.resultType
}
}
}
afterTest.onlyIf {
file("$buildDir/test.result").text == 'SUCCESS'
}
where the afterTest task is executed only when the test suite succeeds.

Passing varargs as second argument in Groovy in Gradle Tooling API

I am trying to execute the Gradle Tooling API, but I am unable to call the addProgressListner() method as I am experiencing surprising issues during compilation:
buildscript {
repositories {
maven { url 'https://repo.gradle.org/gradle/libs-releases' }
}
dependencies {
classpath "org.gradle:gradle-tooling-api:3.1"
classpath 'org.slf4j:slf4j-simple:1.7.10'
}
}
ext {
GRADLE_PROJECT_HOME = '...'
}
import org.gradle.tooling.*
import org.gradle.tooling.events.OperationType
task testGradleToolingAPI {
doLast {
ProjectConnection projectConnection = GradleConnector.newConnector()
.forProjectDirectory(GRADLE_PROJECT_HOME as File)
.connect()
def operationTypes = [OperationType.TASK] as OperationType[]
println "operationTypes.class: ${operationTypes.class}"
projectConnection.newBuild()
.addProgressListener(new ApolloBuildProgressListener(), operationTypes)
.run()
finally {
projectConnection.close()
}
}
}
class ApolloBuildProgressListener implements ProgressListener {
#Override
void statusChanged(ProgressEvent event) {
println "Progress event: ${event.description}"
}
}
The compilation fails as the vararg is not correctly recognized:
operationTypes.class: class [Lorg.gradle.tooling.events.OperationType;
:testGradleToolingAPI FAILED
FAILURE: Build failed with an exception.
* Where:
Build file '/home/martin/devel/tmp/gradle-tooling-api/build.gradle' line: 36
* What went wrong:
Execution failed for task ':testGradleToolingAPI'.
> No signature of method: org.gradle.tooling.internal.consumer.DefaultBuildLauncher.addProgressListener() is applicable for argument types: (ApolloBuildProgressListener, [Lorg.gradle.tooling.events.OperationType;) values: [ApolloBuildProgressListener#7c2dfa2, [TASK]]
Possible solutions: addProgressListener(org.gradle.tooling.events.ProgressListener, [Lorg.gradle.tooling.events.OperationType;), addProgressListener(org.gradle.tooling.ProgressListener), addProgressListener(org.gradle.tooling.events.ProgressListener), addProgressListener(org.gradle.tooling.events.ProgressListener, java.util.Set)
What am I missing?
I'm guessing that ApolloBuildProgressListener is implementing org.gradle.tooling.ProgressListener when it should be org.gradle.tooling.events.ProgressListener. Try this:
class ApolloBuildProgressListener implements org.gradle.tooling.events.ProgressListener {
...
}
Note that the BuildLauncher has four addProgressListener(...) methods. One accepts a org.gradle.tooling.ProgressListener and the other 3 accept a org.gradle.tooling.events.ProgressListener

Custom Gradle Plugin Exec task with extension does not use input properly

I am following the Writing Custom Plugins section of the Gradle documentation, specifically the part about Getting input from the build. The following example provided by the documentation works exactly as expected:
apply plugin: GreetingPlugin
greeting.message = 'Hi from Gradle'
class GreetingPlugin implements Plugin<Project> {
void apply(Project project) {
// Add the 'greeting' extension object
project.extensions.create("greeting", GreetingPluginExtension)
// Add a task that uses the configuration
project.task('hello') << {
println project.greeting.message
}
}
}
class GreetingPluginExtension {
def String message = 'Hello from GreetingPlugin'
}
Output:
> gradle -q hello
Hi from Gradle
I'd like to have the custom plugin execute an external command (using the Exec task), but when changing the task to a type (including types other than Exec such as Copy), the input to the build stops working properly:
// previous and following sections omitted for brevity
project.task('hello', type: Exec) {
println project.greeting.message
}
Output:
> gradle -q hello
Hello from GreetingPlugin
Does anyone know what the issue could be?
It is not related to the type of the task, it's a typical << misunderstanding.
When you write
project.task('hello') << {
println project.greeting.message
}
and execute gradle hello, the following happens:
configuration phase
apply custom plugin
create task hello
set greeting.message = 'Hi from Gradle'
executon phase
run task with empty body
execute << closure { println project.greeting.message }
in this scenario output is Hi from Gradle
When you write
project.task('hello', type: Exec) {
println project.greeting.message
}
and execute gradle hello, the following happens
configuration phase
apply custom plugin
create exec task hello
execute task init closure println project.greeting.message
set greeting.message = 'Hi from Gradle' (too late, it was printed in step 3)
the rest of workflow does not matter.
So, small details matter. Here's the explanation of the same topic.
Solution:
void apply(Project project) {
project.afterEvaluate {
project.task('hello', type: Exec) {
println project.greeting.message
}
}
}

Communication between gradle methods and tasks

I can't seem to get my collection from the method to a state where generateWadleTasks can iterate over it. At least not in a way such that I can access it. I've tried properties, parameters and return values. What's wrong with this picture? I seem to have some misconceptions about how information is passed around either in Groovy or in Gradle... or perhaps both. An explanation of the scopes involved might help. This is a subproject.
buildscript{
...
}
ext.collectionOfObjects = []
class WadleProfile {
def File wadleFile;
def wadlArgs;
}
ext.factoryMethod = {
//supposed to create and add WadleProfiles to collectionOfWadleProfiles
}
compileJava {
doFirst { factoryMethod() }
}
task generateWadlTasks {
println "Executing generateWadlTasks"
project.ext.collectionOfObjects.each { wadlProfile ->
task("wadl2java" + wadlProfile.id, type: JavaExec, dependsOn: 'classes') {
main = project.wadlBaseMain
...
}
}
}

Resources