How to access Junit test counts in Jenkins Pipeline project - jenkins

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.

Related

No such field found: field java.lang.String sinput error when accessing cppcheck plugin classes

]I am a junior dev trying to lear about Jenkins, I have been learning on my own for a couple of months. Currently I have a pipeline (just for learning purposes) which runs static analysis on a folder, and then publish it, I have been able to send a report through email using jelly templates, from there I realized it is posbile to instantiate the classes of a plugin to use its methods so I went to the cppcheck javadoc and did some trial and error so I can get some values of my report and then do something else with them, so I had something like this in my pipeline:
pipeline {
agent any
stages {
stage('analysis') {
steps {
script{
bat'cppcheck "E:/My_project/Source/" --xml --xml-version=2 . 2> cppcheck.xml'
}
}
}
stage('Test'){
steps {
script {
publishCppcheck pattern:'cppcheck.xml'
for (action in currentBuild.rawBuild.getActions()) {
def name = action.getClass().getName()
if (name == 'org.jenkinsci.plugins.cppcheck.CppcheckBuildAction') {
def cppcheckaction = action
def totalErrors = cppcheckaction.getResult().report.getNumberTotal()
println totalErrors
def warnings = cppcheckaction.getResult().statistics.getNumberWarningSeverity()
println warnings
}
}
}
}
}
}
}
which output is:
[Pipeline] echo
102
[Pipeline] echo
4
My logic (wrongly) tells me that if I can access to the report and statistics classes like that and uses their methods getNumberTotal() and getNumberWarningSeverity() respectively, therefore I should be able to also access the DiffState class in the same way and use the valueOf() method to get an enum of the new errors. But adding this to my pipeline:
def nueva = cppcheckaction.getResult().diffState.valueOf(NEW)
println nueva
Gives me an error:
org.jenkinsci.plugins.scriptsecurity.sandbox.RejectedAccessException: No such field found: field org.jenkinsci.plugins.cppcheck.CppcheckBuildAction diffState
at org.jenkinsci.plugins.scriptsecurity.sandbox.groovy.SandboxInterceptor.unclassifiedField(SandboxInterceptor.java:425)
at org.jenkinsci.plugins.scriptsecurity.sandbox.groovy.SandboxInterceptor.onGetProperty(SandboxInterceptor.java:409)
...
I can see in the javadoc there is a diffState class with a valueOf() method, but I cannot access to it is therre any other way to get the new errors between the last build and the current one?
I see 2 issues that could be causing this:
CppcheckResult doesn't have a member variable diffState so you can't access it obviously
If you check the javadoc of CppcheckResult the class does have:
private CppcheckReport report;
public CppcheckStatistics getReport()
and
private CppcheckStatistics statistics;
public CppcheckStatistics getStatistics()
there is no member (and getter method) for diffState so maybe try to call:
/**
* Get differences between current and previous statistics.
*
* #return the differences
*/
public CppcheckStatistics getDiff(){
my suggestion: cppcheckaction.getResult().getDiff().valueOf(NEW). Furthermore CppcheckWorkspaceFile does have a method getDiffState().
Please have a look at the script approval of your Jenkins (see here).
The syntax error might appear because Jenkins (Groovy Sandbox) blocks the execution of an (for the Jenkins) "unknown" and potential dangerous method.
Jenkins settings - Script Approval - Approve your blocked method

How can I abort all jobs with a certain name?

I'm attempting to set up a script to kill/abort all Jenkins jobs with a certain name in them. I've had trouble finding documentation on Jenkins classes and what's contained in them.
I know there are plugins available, but I've been directed not to use them. Otherwise, I've referred to a few semi-related questions here (How to stop an unstoppable zombie job on Jenkins without restarting the server?), (Cancel queued builds and aborting executing builds using Groovy for Jenkins), and I attempted to rework some of the code from those, however it doesn't quite result in killed jobs:
import hudson.model.*
def jobList = Jenkins.instance.queue
jobList.items.findAll { it.task.name.contains('searchTerm') }.each { jobList.kill(it.task) }
I've also tried the following:
def jobname = ""
def buildnum = 85
def job = Jenkins.instance.getItemByFullName(jobname)
for (build in job.builds) {
if (buildnum == build.getNumber().toInteger()){
if (build.isBuilding()){
build.doStop();
build.doKill();
}
}
}
Instead of hard-killing jobs, the first script does nothing, while the second throws a NullPointerException:
java.lang.NullPointerException: Cannot get property 'builds' on null object
I managed to get it working; my second example wasn't working because I brainfarted and the job I was testing it on had no builds. :(
def searchTerm = ""
def matchedJobs = Jenkins.instance.items.findAll { job ->
job.name.contains(searchTerm)
def desiredState = "stop"
if (desiredState.equals("stop")) {
println "Stopping all current builds ${job.name}"
for (build in job.builds) {
if (build.isBuilding()){
build.doStop();
println build.name + " successfully stopped!"
}
}
}

how to end a jenkins build with a specific parameter

I trigger a build called testAUT with parameter FRACTURE_NO = 15. I trigger the same build with the same parameter.
I want when that happens that the earlier build gets killed. How can I do that using Jenkins? I want to be able to end a build with the same parameter as the current build.
If you're OK with using the Groovy Plugin and Jenkins Rest Api, here are the steps needed to do what you want.
Install the Groovy Plugin.
Go to the job's configure page.
Add the 'fracture_no' build parameter with default value of 125.
Enable 'Execute concurrent builds if necessary' in the General section
Add a 'Execute Groovy script' build step as the first step in the job
Add the following code to the groovy step
def parameterName = "fracture_no";
def jenkinsUrl = System.getenv('JENKINS_URL');
def buildNumber = System.getenv('BUILD_NUMBER').toInteger();
def jobUrl = jenkinsUrl + "job/" + System.getenv('JOB_NAME');
def buildNumberUrl = jobUrl + "/" + buildNumber;
def myParameter = System.getenv(parameterName);
def projectXml = new XmlSlurper().parseText(new URL(jobUrl + "/api/xml").getText());
projectXml.build.each {
def previousBuildNumber = it.number.text().toInteger();
if(previousBuildNumber < buildNumber)
{
def previousBuildNumberUrl = jobUrl + "/" + previousBuildNumber;
def jobXml = new XmlSlurper().parseText(new URL(previousBuildNumberUrl + "/api/xml").getText());
if(jobXml.building.text() == "true")
{
jobXml.action.parameter.each {
if(it.name.text() == parameterName) {
if(it.value.text() == myParameter) {
def url = new URL(previousBuildNumberUrl + "/stop");
def connection = url.openConnection();
connection.setRequestMethod("POST");
connection.connect();
connection.content.text;
println "Stopping " + previousBuildNumber;
}
}
}
}
}
}
Add another build step that takes a long time. If you just want to test, then add the following Windows Batch Command step that sleeps for 100 seconds.
ping 127.0.0.1 -n 100 > nul
Kick off the job twice using the default parameter value of 125 (or as many times as you want).
Everything but the latest build should be stopped.

Jenkins: Send email based on script output

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.

Call a jenkins job by using a variable for build the name

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")

Resources