Sunday, October 4, 2009

Fancy Hudson Email

Over lunch I was discussing email overload with my coworker Scott. I already use automatic color-coding and move-to-folder rules to help me find the important stuff at a glance. Still there are some automated emails that I must read, but which are time-consuming to deal with.

One such email is the Hudson build result. Hudson is a fantastic tool for continuous integration, but its email format leaves a lot to be desired:



From there I have to click the link, wait for a slow loading Hudson page, then click another link to view the full test results. Then I have to sift through the tests that only failed once because of environmental quirks and Selenium bugs, to identify the recurring test failures that need prompt attention.

What I really want is something like this:



The subject line tells how many tests have failed. The recurring test failures are listed with direct links to each failure message. If there are no recurring failures then the test is marked a success. The duration is red for tests that took over a minute. If the test name and error message are short enough, the error message is displayed.

Fortunately Scott had read a blog post with a solution that might allow us to write our own email output in Groovy. I googled it and found this gem by Chetan: Using Groovy with Hudson to send rich text email

If you read Chetan's post and the comments, most problems come from trying to show recent SCM changes in Perforce, SVN, or CVS. That's not my top priority, so I'm starting with the output that I described above. Here's how I got it working:
  1. Download Chetan's enhanced email-ext plugin for Hudson and add it to your Hudson setup at Hudson -> Manage Hudson -> Manage Plugins -> Advanced
  2. Upgrade Hudson to the latest version (1.326 when I started the project 2 days ago, although 1.327 came out last night, because Kohsuke Kawaguchi has superpowers and releases enhancements all the time)
  3. Disable "E-mail Notification" and enable "Editable Email Notification"
  4. Enable "Default Content is Script" and "Default Content Type is HTML"
  5. Set the default subject to this Groovy template:



    $DEFAULT_SUBJECT <% def tr = build.testResultAction;  if (tr?.failCount) { %>(${tr?.failCount} failures ${tr?.failureDiffString}) <% } %>



  6. Set the default content to this Groovy template, which you'll probably want to read and edit to suit your needs:
<style>
body, table, td, th, p { font-family: Verdana,Helvetica,sans serif; font-size: 11px; }
.pane      { margin-top: 4px; white-space: nowrap; }
table.pane { border: 1px solid #BBB; border-collapse: collapse; }
td.pane    { border: 1px solid #BBB; padding: 3px 4px; vertical-align: middle; }
th         { border: 1px solid #BBB; background-color: #F0F0F0; font-weight: bold; padding: 4px; }
</style>
<%
def stillFailing = []
def rootUrl = hudson.model.Hudson.instance.rootUrl
def jobName = build.parent.name
def buildNumber = build.number
def buildUrl = "${rootUrl}job/$jobName/$buildNumber/testReport/"
if (build.testResultAction) {
    build.testResultAction.failedTests.each{tr ->
        def packageName = tr.packageName
        def simpleClassName = tr.simpleName
        def testName = tr.safeName
        def displayName = tr.className+"."+testName
        def duration = tr.durationString;
        if (duration.contains(" min")) {
            duration = """<font color="red">""" + duration + "</font>"
        }
        def url = "${rootUrl}job/$jobName/$buildNumber/testReport/$packageName/$simpleClassName/$testName"
        def error = (tr.errorDetails && tr.errorDetails.length() < 30 && displayName.length() < 100) ? tr.errorDetails : ""
        error = error.replaceAll("<", "&lt;")
        def failMap = [displayName:displayName,url:url,age:tr.age,error:error,duration:duration]
        if (tr.age > 1) {
            stillFailing << failMap
        }
    }
    stillFailing = stillFailing.sort {it.displayName}
}
def emailHeader = stillFailing.size() > 0 ? "Recurring Failed Tests" : "Success"
%>
$PROJECT_NAME - Build # $BUILD_NUMBER - $BUILD_STATUS:<br/>
<br/>
Check <a href="${buildUrl}">${buildUrl}</a> to view the results.<br/>
<h2>${emailHeader}</h2>
<table class="pane">
    <tr>
        <th>Test Name</th>
        <th>Duration</th>
        <th>Age</th>
    </tr>
    <% stillFailing.each { failedTest-> %>
        <tr>
            <td class="pane"><a href="${failedTest.url}">${failedTest.displayName}</a>&nbsp;&nbsp;${failedTest.error}</td>
            <td class="pane" style="text-align: right;">${failedTest.duration}</td>
            <td class="pane" style="text-align: right;">${failedTest.age}</td>
        </tr>
    <% } %>
</table>

Chetan has submitted a patch for Hudson to have this ability without hacking the email-ext plugin. Maybe someday this will be even more trivial to set up.

It's easier to edit the Groovy code in the Hudson configure page textarea if that textarea is wider and uses a monospace font. To that end, I've written a Greasemonkey script called monospace-hudson that makes those changes when the configure page loads.

Without monospace-hudson


With monospace-hudson




2 comments:

3arbia said...

Thank you very much for this blog.
I tried to follow the steps described above but it does not work.
Just what I have done in the hudson manger page:
1. Default subject
$DEFAULT_SUBJECT <% def tr = build.testResultAction; if (tr?.failCount) { %>(${tr?.failCount} failures ${tr?.failureDiffString}) <% } %>
2. Default content:
I copied yours

the mail that I have received has this content:
<% def stillFailing = [] def rootUrl = hudson.model.Hudson.instance.rootUrl def jobName = build.parent.name def buildNumber = build.number def buildUrl = "${rootUrl}job/$jobName/$buildNumber/testReport/" if (build.testResultAction) { build.testResultAction.failedTests.each{tr -> def packageName = tr.packageName def simpleClassName = tr.simpleName def testName = tr.safeName def displayName = tr.className+"."+testName def duration = tr.durationString; if (duration.contains(" min")) { duration = """""" + duration + "" } def url = "${rootUrl}job/$jobName/$buildNumber/testReport/$packageName/$simpleClassName/$testName" def error = (tr.errorDetails && tr.errorDetails.length() < 30 && displayName.length() < 100) ? tr.errorDetails : "" error = error.replaceAll("<", "<") def failMap = [displayName:displayName,url:url,age:tr.age,error:error,duration:duration] if (tr.age > 1) { stillFailing << failMap } } stillFailing = stillFailing.sort {it.displayName} } def emailHeader = stillFailing.size() > 0 ? "Recurring Failed Tests" : "Success" %> sonar_test - Build # 66 - Successful:

Check ${buildUrl} to view the results.
${emailHeader}
Test Name Duration Age
<% stillFailing.each { failedTest-> %>
${failedTest.displayName} ${failedTest.error} ${failedTest.duration} ${failedTest.age}
<% } %>

Please any help

3arbia said...

thank you for this blog.
I have followed the steps described bellow but it does not work.
That is what I have configured in the hudson manager page.
1. Copy the default subject (yours)
2. Copy the default content subject
The mail that I have received has this format:
<% def stillFailing = [] def rootUrl = hudson.model.Hudson.instance.rootUrl def jobName = build.parent.name def buildNumber = build.number def buildUrl = "${rootUrl}job/$jobName/$buildNumber/testReport/" if (build.testResultAction) { build.testResultAction.failedTests.each{tr -> def packageName = tr.packageName def simpleClassName = tr.simpleName def testName = tr.safeName def displayName = tr.className+"."+testName def duration = tr.durationString; if (duration.contains(" min")) { duration = """""" + duration + "" } def url = "${rootUrl}job/$jobName/$buildNumber/testReport/$packageName/$simpleClassName/$testName" def error = (tr.errorDetails && tr.errorDetails.length() < 30 && displayName.length() < 100) ? tr.errorDetails : "" error = error.replaceAll("<", "<") def failMap = [displayName:displayName,url:url,age:tr.age,error:error,duration:duration] if (tr.age > 1) { stillFailing << failMap } } stillFailing = stillFailing.sort {it.displayName} } def emailHeader = stillFailing.size() > 0 ? "Recurring Failed Tests" : "Success" %> sonar_test - Build # 66 - Successful:

Check ${buildUrl} to view the results.
${emailHeader}
Test Name Duration Age
<% stillFailing.each { failedTest-> %>
${failedTest.displayName} ${failedTest.error} ${failedTest.duration} ${failedTest.age}
<% } %>

Followers