I have tests automation framework, and the test runs are driven through Jenkins. After the test run is completed, it generates a few custom test reports, as HTML file. In the report, I have a table with a total number of errors as one of the columns. I basically want to parse this HTML file and send out an email to a mailing list if the error is greater than 0.
Here is what I have done till now, in my Jenkins job:
a. Configured the job to run the tests [this is in an execute shell option and running my jar, as the test framework is a java application]
b. I have a post-build action, in which I execute a python code, which does the following:
Step 1. Get the latest HTML report.
Step 2. Parse this file.
Step 3. If the error is > 0:
then email_flag = true
else:
email_flag = false.
Now is there a way I can use the above flags and send email notifications, via Jenkins plugins? I just want to understand the best way to do this. Any help is appreciated.
P.S. I did some additional research and I see the Email-ext plugin has a pre-send script option, but I just don't know how to use this to run the python code and handle the email messaging.
During your build action - ie while executing the tests, when you know the test fails in java, immediately or after executing all the tests , you can make java return a value to the shell.
System.exit(0) - Success
System.exit(1) - Failure
Whenever it is not 0, jenkins jobs is marked as Failed by default.
You can use Email-ext plugin to send the email only when the job is failed by selecting the appropriate trigger - Failure - Any
Below is my Extended Email Publisher Jenkins plugin pre-send Groovy script. Hope you are able to adjust it to your needs.
It is designed for parsing Serenity-JUnit XML files but with little effort it can parse HTML. It will look for any file in given sourceDirPath which match pattern "SERENITY-JUNIT-*xml". It will then file.readToString(), extract test results from that xmlString and aggregate those results into variables tests, passed, failures, errors, skipped. Finally it will compose a msgSubject and append it to msg itself.
NOTE that build, logger and msg are objects made available by the plugin. You should not change assign them and/or change their names.
Script:
import static groovy.io.FileType.FILES
import javax.xml.parsers.DocumentBuilderFactory
import javax.xml.parsers.DocumentBuilder
import org.w3c.dom.Document
import org.w3c.dom.NodeList
import org.w3c.dom.Node
import org.w3c.dom.Element
import org.xml.sax.InputSource
import java.io.StringReader
logger.println "------------------------------------------------------------------------------------------------"
logger.println "Parsing SERENITY-JUNIT XML files for results to update Editable Email Notification."
// determine if build is remote, list folder and files
def sourceDirPath
if(build.workspace.isRemote()){
channel = build.workspace.channel
logger.println "Using channel: " + channel
sourceDirPath = new FilePath(channel, build.workspace.toString() + "\\target\\site\\serenity\\")
} else {
sourceDirPath = new FilePath(new File(build.workspace.toString() + "\\target\\site\\serenity\\"))
}
logger.println "Source dir: " + sourceDirPath
def files = sourceDirPath.list("SERENITY-JUNIT-*xml")
logger.println "Found XML files: \n" + files
// initialize result variables
def tests = 0
def failures = 0
def errors = 0
def skipped = 0
def passed = 0
// parse files
DocumentBuilderFactory dbFactory = DocumentBuilderFactory.newInstance()
files.each{
logger.println "Starting to parse file: " + it
channel = build.workspace.channel
def file = new FilePath(channel, it.getRemote())
def xmlString = file.readToString()
logger.println "Extracted XML:\n" + xmlString
DocumentBuilder dBuilder = dbFactory.newDocumentBuilder()
Document doc = dBuilder.parse(new InputSource(new StringReader(xmlString)))
doc.getDocumentElement().normalize()
Element testsuiteNode = doc.getDocumentElement()
logger.println testsuiteNode
tests += Integer.parseInt(testsuiteNode.getAttribute("tests"))
failures += Integer.parseInt(testsuiteNode.getAttribute("failures"))
errors += Integer.parseInt(testsuiteNode.getAttribute("errors"))
skipped += Integer.parseInt(testsuiteNode.getAttribute("skipped"))
logger.println "Completed parsing file: " + it
}
// calculate passed tests amount
passed = tests - failures - errors - skipped
// aggregated results
logger.println "Aggregated results:"
logger.println "Tests: " + tests
logger.println "Passed: " + passed
logger.println "Failures: " + failures
logger.println "Errors: " + errors
logger.println "Skipped: " + skipped
// compose message subject
def msgSubject
if(tests == 4){
if(failures > 0 || errors > 0){
msgSubject = "FAILED - Passed ${passed}/${tests}, Failures ${failures}, Errors ${errors}, Skipped ${skipped}."
}else if(skipped > 0){
msgSubject = "OK (w/ skipped tests) - Passed ${passed}/${tests}, Failures ${failures}, Errors ${errors}, Skipped ${skipped}."
}else{
msgSubject = "OK - Passed ${passed}/${tests}, Failures ${failures}, Errors ${errors}, Skipped ${skipped}."
}
}else{
msgSubject = "FAILED - Not all tests or too many tests executed (${tests})."
}
// set message subject
msg.setSubject(msg.getSubject() + ": " + msgSubject)
logger.println "------------------------------------------------------------------------------------------------"
You may of course dynamically change any element of the email (To, From, MsgText, etc.). Here I am only changing the subject.
The result is something like:
Wellness Check: OK - Passed 4/4, Failures 0, Errors 0, Skipped 0.
Related
I want to delete old builds for all the jobs I kept inside a folder from the script console, not from Jenkins individual job configuration to discard old builds. I want to keep only the recent 10 builds and remove the older ones. I am asking about the deletion of the old builds of a particular folder and not other jobs in another folder.
The following groovy script will delete all the builds excluding the 10 most recent builds of each job configured in Jenkins.
import jenkins.model.*
import hudson.model.*
Jenkins.instance.getAllItems(AbstractItem.class).each {
def job = jenkins.model.Jenkins.instance.getItem(it.fullName)
if (job!=null){
println "job: " + job;
int i = 1;
job.builds.each {
def build = it;
if (i<=10){
println "build: " + build.displayName + " will NOT BE DELETED";
}else{
println "build: " + build.displayName + " will BE DELETED";
it.delete();
}
i = ++i
}
}
};
The first loop will iterate over all Jenkins items and will store it under job var. If the job is not a Jenkins Job the result can be null, so the if (job!=null) check is there to allow the code to proceed only for Jenkins jobs.
The int i = 1; is the initialization of a counter for each job, that is incremented when a build is found. As far as I know, the build order is preserved, and the most recent build is returned first in the loop. When the counter reaches 10 or more, the if..else enters the else block and deletes the build it.delete()
I wrote a plugin following
http://www.baeldung.com/jenkins-custom-plugin
And it generates a html report
File artifactsDir = build.getArtifactsDir();
String path = artifactsDir.getCanonicalPath() + REPORT_TEMPLATE_PATH;
File reportFile = new File("path");
// write report's text to the report's file
and for the next build, I want to import this report file to see the changes
I tried these but none of them works
build.getPreviousSuccessfulBuild().getArtifactManager().root() + REPORT_TEMPLATE_PATH
// fail with File not found, but the file is there in bash
build.getPreviousSuccessfulBuild().getArtifactsDir() + REPORT_TEMPLATE_PATH
// null pointer exception, seems to be generated by getArtifactsDir()
build.getPreviousBuild().getArtifactManager().root() + REPORT_TEMPLATE_PATH
So how can I obtain the last successful build report file within current build ?
This is how I did it in an pipeline job. I stripped parts of the original code, I hope I didn't introduce an error. I also removed error handling for clarity:
// find last successful build
def lastSuccessfulBuild = currentBuild.getPreviousBuild()
while (lastSuccessfulBuild && (lastSuccessfulBuild.currentResult != 'SUCCESS')) {
lastSuccessfulBuild = lastSuccessfulBuild.getPreviousBuild()
}
// here I go for a file named 'crc.txt'
// this works only if you have a
// archiveArtifacts artifacts: 'crc.txt', fingerprint: true
// somewhere in your build
def build = lastSuccessfulBuild?.getRawBuild()
def artifact = build.getArtifacts().find { it.fileName == 'crc.txt' }
def uri = build.artifactManager.root().child(artifact.relativePath).toURI()
def content = uri.toURL().text
When I compare our solutions: you don't use child() and you have the relative path in REPORT_TEMPLATE_PATH while I obtain it from the artifact.
I have my jmeter script running from a jenkins job but it is always reporting it as failed even though the script actually passed. I am using the 'Publish Performance test result report Post-build Action. Please see screenshot. What am I doing wrong? Why does it always show an error even though the actual jmeter script is passing?
You set the threshold for Unstable/Failure to 0, so even 0 consider as failure.
Increase the thresholds for Unstable and Failed
I found a way to do this! Use this plugin: https://wiki.jenkins.io/display/JENKINS/Log+Parser+Plugin
In a rules file containing this line:
error /FAILED/
In the script add a beanshell Assertion for each HTTP Request that will log messages for successes and failures. Here's one example:
String testscenario = "TEST SCENARIO: ";
String requestName = "Find Stuff";
String findStuff = "${MyStuff}";
String custName = "${CustName}";
String respData = new String(ResponseData);
if (respData.contains(findStuff))
{
log.info(testscenario+"Passed. "+requestName+" MyStuff for Cust: " +
custName + " containing " + findStuff + " returned.");
print(testscenario+"Passed. "+requestName+" MyStuff for Customer " +
custName + " containing " + findStuff + " returned.");
} else
{
log.error(testscenario+"*FAILED. "+requestName);
print(testscenario+"*FAILED. "+requestName);
}
I have just started with Jenkins
My freestyle project used to report JUnit tests results in Slack like this
MyJenkinsFreestyle - #79 Unstable after 4 min 59 sec (Open)
Test Status:
Passed: 2482, Failed: 13, Skipped: 62
Now I have moved the same to pipeline project, and all is good except that Slack notifications do not have Test Status
done MyPipelineProject #68 UNSTABLE
I understand I have to construct the message to send to Slack, and I have done that above for now.
The only issue is how do I read the test status - the passed count, failed count etc.
This is called "test summary" in Jenkins slack-plugin commit, and here is the screenshot
So how do I access Junit tests count/details in Jenkins Pipeline project ? - so that these are reported in notifications.
UPDATE:
In the Freestyle project, the Slack notification itself has the "test summary", and there is no option to opt (or not) for the test summary.
In Pipeline project, my "junit" command to "Publish JUnit test results" is before sending Slack notification.
So in code those lines look like this (this are last lines of the last stage):
bat runtests.bat
junit 'junitreport/xml/TEST*.xml'
slackSend channel: '#testschannel', color: 'normal', message: "done ${env.JOB_NAME} ${env.BUILD_NUMBER} (<${env.BUILD_URL}|Open>)";
For anyone coming here in 2020, there appears to be a simpler way now. The call to 'junit testResults' returns a TestResultSummary object, which can be assigned to a variable and used later.
As an example to send the summary via slack:
def summary = junit testResults: '/somefolder/*-reports/TEST-*.xml'
slackSend (
channel: "#mychannel",
color: '#007D00',
message: "\n *Test Summary* - ${summary.totalCount}, Failures: ${summary.failCount}, Skipped: ${summary.skipCount}, Passed: ${summary.passCount}"
)
From this presentation of Cloudbees I found that it should be possible via "build" object.
It has code like
def testResult = build.testResultAction
def total = testResult.totalCount
But currentBuild does not provide access to testResultAction.
So kept searching and found this post "react on failed tests in pipeline script".
There Robert Sandell has given "pro tip"
Pro tip, requires some "custom whitelisting":
AbstractTestResultAction testResultAction = currentBuild.rawBuild.getAction(AbstractTestResultAction.class)
if (testResultAction != null) {
echo "Tests: ${testResultAction.failCount} / ${testResultAction.failureDiffString} failures of ${testResultAction.totalCount}.\n\n"
}
This worked like a charm - just that I had to deselect "Groovy sandbox" checkbox.
Now I have these in the build log
Tests: 11 / ±0 failures of 2624
Now I will use this to prepare string to notify in slack with test results.
UPDATE:
Finally, the function I used to get output like the following
(Note the "failure diff" after failed tests is very useful)
Test Status:
Passed: 2628, Failed: 6 / ±0, Skipped: 0
Is the following:
import hudson.tasks.test.AbstractTestResultAction
#NonCPS
def testStatuses() {
def testStatus = ""
AbstractTestResultAction testResultAction = currentBuild.rawBuild.getAction(AbstractTestResultAction.class)
if (testResultAction != null) {
def total = testResultAction.totalCount
def failed = testResultAction.failCount
def skipped = testResultAction.skipCount
def passed = total - failed - skipped
testStatus = "Test Status:\n Passed: ${passed}, Failed: ${failed} ${testResultAction.failureDiffString}, Skipped: ${skipped}"
if (failed == 0) {
currentBuild.result = 'SUCCESS'
}
}
return testStatus
}
UPDATE 2018-04-19
Note the above require manual "whitelisting" of methods used.
Here is how you can whitelist all the methods in one go
Manually update the whitelist...
Exit Jenkins
Create/Update %USERPROFILE%.jenkins\scriptApproval.xml with the following content
<?xml version='1.0' encoding='UTF-8'?>
<scriptApproval plugin="script-security#1.23">
<approvedScriptHashes>
</approvedScriptHashes>
<approvedSignatures>
<string>method hudson.model.Actionable getAction java.lang.Class</string>
<string>method hudson.model.Cause getShortDescription</string>
<string>method hudson.model.Run getCauses</string>
<string>method hudson.tasks.test.AbstractTestResultAction getFailCount</string>
<string>method hudson.tasks.test.AbstractTestResultAction getFailureDiffString</string>
<string>method hudson.tasks.test.AbstractTestResultAction getSkipCount</string>
<string>method hudson.tasks.test.AbstractTestResultAction getTotalCount</string>
<string>method org.jenkinsci.plugins.workflow.support.steps.build.RunWrapper getRawBuild</string>
</approvedSignatures>
<aclApprovedSignatures/>
<approvedClasspathEntries/>
<pendingScripts/>
<pendingSignatures/>
<pendingClasspathEntries/>
</scriptApproval>
Restart Jenkins
and then verify that the "In script approval" has the above entries approved
NOTE: Its the which is important. So if the scriptApproval file is already there, then you will generally need to ensure the contents of tag.
To expand upon #vikramsjn's answer, here is what I use to get the test summary in my Jenkinsfile:
import hudson.tasks.test.AbstractTestResultAction
import hudson.model.Actionable
#NonCPS
def getTestSummary = { ->
def testResultAction = currentBuild.rawBuild.getAction(AbstractTestResultAction.class)
def summary = ""
if (testResultAction != null) {
def total = testResultAction.getTotalCount()
def failed = testResultAction.getFailCount()
def skipped = testResultAction.getSkipCount()
summary = "Test results:\n\t"
summary = summary + ("Passed: " + (total - failed - skipped))
summary = summary + (", Failed: " + failed)
summary = summary + (", Skipped: " + skipped)
} else {
summary = "No tests found"
}
return summary
}
I then use this method to instantiate my testSummary variable:
def testSummary = getTestSummary()
This will return something similar to:
"Test results:
Passed: 123, Failed: 0, Skipped: 0"
First of all, thank you for above answers. They saved me a lot of time, I used proposed solution in my pipeline. I however didn't use "whitelisting" and it works fine.
I use shared libraries for Jenkins pipeline and here is a piece of that shared library with pipeline and using of methods to get counts:
import hudson.model.*
import jenkins.model.*
import hudson.tasks.test.AbstractTestResultAction
def call(Closure body) {
...
def emailTestReport = ""
pipeline {
...
stages{
stage('Test'){
...
post {
always {
junit 'tests.xml'
script {
AbstractTestResultAction testResultAction = currentBuild.rawBuild.getAction(AbstractTestResultAction.class)
if (testResultAction != null) {
def totalNumberOfTests = testResultAction.totalCount
def failedNumberOfTests = testResultAction.failCount
def failedDiff = testResultAction.failureDiffString
def skippedNumberOfTests = testResultAction.skipCount
def passedNumberOfTests = totalNumberOfTests - failedNumberOfTests - skippedNumberOfTests
emailTestReport = "Tests Report:\n Passed: ${passedNumberOfTests}; Failed: ${failedNumberOfTests} ${failedDiff}; Skipped: ${skippedNumberOfTests} out of ${totalNumberOfTests} "
}
}
mail to: 'example#email.com',
subject: "Tests are finished: ${currentBuild.fullDisplayName}",
body: "Tests are finished ${env.BUILD_URL}\n Test Report: ${emailTestReport} "
}
}
}
}
}
}
p.s. If I create emailTestRepot as a local variable inside script "section" I get next exception:
an exception which occurred:
in field locals
in field parent
in field caller
in field e
in field program
in field threads
in object org.jenkinsci.plugins.workflow.cps.CpsThreadGroup#11cd92de
Caused: java.io.NotSerializableException: hudson.tasks.junit.TestResultAction
...
I struggled a lot with trying to fix that java.io.NotSerializableException. As I understood I needed to use "whitelisting" to prevent NotSerializableException. But I really didn't want to do it and when I moved "def emailTestReport" out of pipeline it worked just fine.
I try to launch a job from a parametrized trigger and I would compute the name from a given variable.
Is it possible to set in field :
Build Triggers Projects to build
a value like this
${RELEASE}-MAIN-${PROJECT}-LOAD_START
?
Unfortunately, this isn't possible with the Build Triggers. I looked for a solution for this "higher order build job" that would allow you to create a dynamic build name with a one of the parameterized build plugins, but I couldn't find one.
However, using the Groovy Postbuild Plugin, you can do a lot of powerful things. Below is a script that can be modified to do what you want. In particular, notice that it gets environmental variables using build.buildVariables.get("MY_ENV_VAR"). The environmental variable TARGET_BUILD_JOB specifies the name of the build job to build. In your case, you would want to build TARGET_BUILD_JOB using these two environmental variables:
build.buildVariables.get("RELEASE")
build.buildVariables.get("PROJECT")
The script is commented so that if you're not familiar with Groovy, which is based off Java, it should hopefully make sense!
import hudson.model.*
import hudson.model.queue.*
import hudson.model.labels.*
import org.jvnet.jenkins.plugins.nodelabelparameter.*
def failBuild(msg)
{
throw new RuntimeException("[GROOVY] User message, exiting with error: " + msg)
}
// Get the current build job
def thr = Thread.currentThread()
def build = thr?.executable
// Get the parameters for the current build job
// For ?:, see "Elvis Operator" (http://groovy.codehaus.org/Operators#Operators-ElvisOperator)
def currentParameters = build.getAction(ParametersAction.class)?.getParameters() ?:
failBuild("There are no parameters to pass down.")
def nodeName = build.getBuiltOnStr()
def newParameters = new ArrayList(currentParameters); newParameters << new NodeParameterValue("param_NODE",
"Target node -- the node of the previous job", nodeName)
// Retrieve information about the target build job
def targetJobName = build.buildVariables.get("TARGET_BUILD_JOB")
def targetJobObject = Hudson.instance.getItem(targetJobName) ?:
failBuild("Could not find a build job with the name $targetJobName. (Are you sure the spelling is correct?)")
println("$targetJobObject, $targetJobName")
def buildNumber = targetJobObject.getNextBuildNumber()
// Add information about downstream job to log
def jobUrl = targetJobObject.getAbsoluteUrl()
println("Starting downstream job $targetJobName ($jobUrl)" + "\n")
println("======= DOWNSTREAM PARAMETERS =======")
println("$newParameters")
// Start the downstream build job if this build job was successful
boolean targetBuildQueued = targetJobObject.scheduleBuild(5,
new Cause.UpstreamCause(build),
new ParametersAction(newParameters)
);
if (targetBuildQueued)
{
println("Build started successfully")
println("Console (wait a few seconds before clicking): $jobUrl/$buildNumber/console")
}
else
failBuild("Could not start target build job")