Skip to content
Permalink
master
Switch branches/tags
Go to file
 
 
Cannot retrieve contributors at this time
#!/usr/bin/env groovy
/*
* See the NOTICE file distributed with this work for additional
* information regarding copyright ownership.
*
* This is free software; you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as
* published by the Free Software Foundation; either version 2.1 of
* the License, or (at your option) any later version.
*
* This software is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this software; if not, write to the Free
* Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
* 02110-1301 USA, or see the FSF site: http://www.fsf.org.
*/
import hudson.FilePath
import hudson.util.IOUtils
import javax.xml.bind.DatatypeConverter
import hudson.tasks.test.AbstractTestResultAction
import com.cloudbees.groovy.cps.NonCPS
import java.text.SimpleDateFormat
// If you need to setup a Jenkins instance where the following script will work you'll need to:
//
// - Configure Global tools:
// - One named 'Maven' for Maven,
// - One named 'java7' for Java 7
// - One named 'official' for Java 8
// - Configure a Global Pipeline library
// - Name: 'XWiki'
// - Version: 'master'
// - Enable 'Load implicitly'
// - Choose modern SCM and then GitHub:
// - owner: 'xwiki'
// - repository: 'xwiki-jenkins-pipeline'
// - Have the following plugins installed:
// - XVnc plugin. You'll also need to have the "vncserver" executable available in the path
// - Email Extension plugin (provides emailext() API)
// - Build Timeout plugin (provides timeout() API)
// - Pipeline Utility Steps plugin (provides readMavenPom() API)
// - Pipeline Maven Integration plugin (provides withMaven() API)
// - Groovy Post Build plugin (provides the 'manager' variable)
/**
* @param name a string representing the current build
*/
void call(name = 'Default', body)
{
echoXWiki "Calling Jenkinsfile for build [{$name}]"
def config = [:]
body.resolveStrategy = Closure.DELEGATE_FIRST
body.delegate = config
body()
printConfigurationProperties(config)
// Does the following:
// - Only keep builds for the last configured number of days (7 by default)
// - Disable concurrent builds to avoid rebuilding whenever a new commit is made. The commits will accumulate till
// the previous build is finished before starting a new one.
// Note 1: this is limiting concurrency per branch only.
// Note 2: This needs to be one of the first code executed which is why it's the first step we execute.
// See https://thepracticalsysadmin.com/limit-jenkins-multibranch-pipeline-builds/ for details.
// - Make sure projects are built at least once a month because SNAPSHOT older than one month are deleted
// by the Nexus scheduler.
def computedDaysToKeepStr = config.daysToKeepStr ?: '7'
echoXWiki "Only keep the builds for the last $computedDaysToKeepStr days + disable concurrent builds"
def projectProperties = [
[$class: 'BuildDiscarderProperty', strategy: [$class: 'LogRotator', daysToKeepStr: computedDaysToKeepStr]],
disableConcurrentBuilds(),
pipelineTriggers([cron("@monthly")])
]
// Process job properties overrides.
def allProperties = []
// Note: we add the overridden job properties first since the properties() step will honor the values that come
// first and ignore further ones. This allows the Jenkinsfile to take precedence.
echoXWiki "Custom job properties: ${config.jobProperties}"
if (config.jobProperties) {
allProperties.addAll(config.jobProperties)
}
allProperties.addAll(projectProperties)
echoXWiki "Full job properties: ${allProperties}"
properties(allProperties)
def mavenTool
def javaMavenConfig
stage("Preparation for ${name}") {
// Get the Maven tool.
// Note: We use an empty string by default, in order to not use the Global tools from Jenkins. We run our
// builds on the XWiki Docker build image which has Maven pre-installed. If the caller passes a non-empty string
// then the Maven setup will need to be defined in Jenkins's Global tools.
mavenTool = config.mavenTool?.trim() ?: ''
// Check if the build should be aborted
if (config.disabled) {
currentBuild.result = 'ABORTED'
error "Aborting build since it's disabled explicitly..."
}
if (!config.skipCheckout) {
echoXWiki "SCM checkout with changelog set to [${config.skipChangeLog}]"
checkout changelog: config.skipChangeLog ?: false, scm: scm
}
// Configure the version of Java to use
def pom = readMavenPom file: getPOMFile(config)
javaMavenConfig = configureJavaTool(config, pom)
// Generate a ~/.docker/config.json file containing authentication data for Dockerhub so that all operations
// done on Dockerhub are done while authenticated, which prevents the pull-rate issue.
def dockerHubSecretId = config.dockerHubSecretId == null ? 'xwikici' : config.dockerHubSecretId
def dockerHubUserId = config.dockerHubUserId ?: 'xwikici'
generateDockerConfig(dockerHubSecretId, dockerHubUserId)
// Display some environmental information that can be useful to debug some failures
// Note: if the executables don't exist, this won't fail the step thanks to "returnStatus: true".
sh script: 'ps -ef', returnStatus: true
sh script: 'netstat -nltp', returnStatus: true
// Note: the "|| true" allows the sh command to fail (when firefox is not installed for example) without
// failing the job build.
def firefoxVersion = sh script: 'firefox -version || true', returnStdout: true
if (firefoxVersion) {
echoXWiki "Firefox version installed: ${firefoxVersion}"
}
}
stage("Build for ${name}") {
// Execute the XVNC plugin (useful for integration-tests)
wrapInXvnc(config) {
// Execute the Maven build.
// Note that withMaven() will also perform some post build steps:
// - Archive and fingerprint generated Maven artifacts and Maven attached artifacts (if archiveArtifacts
// is set to true, see above)
// - Publish JUnit / Surefire reports (if the Jenkins JUnit Plugin is installed)
// - Publish Findbugs reports (if the Jenkins FindBugs Plugin is installed)
// - Publish a report of the tasks ("FIXME" and "TODO") found in the java source code
// (if the Jenkins Tasks Scanner Plugin is installed)
echoXWiki "Using Java tool: ${javaMavenConfig.jdk}"
echoXWiki "Using Maven tool: ${mavenTool ?: 'None, using pre-installed mvn executable on host'}"
echoXWiki "Using Maven options: ${javaMavenConfig.mavenOpts}"
def archiveArtifacts = config.archiveArtifacts == null ? false : config.archiveArtifacts
echoXWiki "Artifact archiving: ${archiveArtifacts}"
def fingerprintDependencies = config.fingerprintDependencies == null ? false :
config.fingerprintDependencies
echoXWiki "Dependencies fingerprinting: ${fingerprintDependencies}"
// Note: We're not passing "mavenOpts" voluntarily, see configureJavaTool()
def publishers = [
artifactsPublisher(disabled: !archiveArtifacts),
dependenciesFingerprintPublisher(disabled: !fingerprintDependencies)
]
// Note: withMaven is concatenating any passed "mavenOpts" with env.MAVEN_OPTS. Thus in order to fully
// control the maven options used we set env.MAVEN_OPTS to empty.
env.MAVEN_OPTS = ''
withMaven(maven: mavenTool, jdk: javaMavenConfig.jdk, mavenOpts: javaMavenConfig.mavenOpts,
options: publishers)
{
try {
def goals = computeMavenGoals(config)
echoXWiki "Using Maven goals: ${goals}"
def profiles = getMavenProfiles(config, env)
echoXWiki "Using Maven profiles: ${profiles}"
def properties = getMavenSystemProperties(config, "${NODE_NAME}")
echoXWiki "Using Maven properties: ${properties}"
def javadoc = ''
if (config.javadoc == null || config.javadoc == true) {
javadoc = 'javadoc:javadoc -Ddoclint=all'
echoXWiki "Enabling javadoc validation"
}
def timeoutThreshold = config.timeout ?: 240
def sdf = new SimpleDateFormat('yyyy-MM-dd HH:mm:ss')
echoXWiki "Using timeout: [${timeoutThreshold}] minutes. Starting at [${sdf.format(new Date())}]"
// Display the java version for information (in case it's useful to debug some specific issue)
echoXWiki 'Java version used:'
sh script: 'java -version', returnStatus: true
// Abort the build if it takes more than the timeout threshold (in minutes).
timeout(timeoutThreshold) {
def pom = getPOMFile(config)
echoXWiki "Using POM file: ${pom}"
def mavenFlags = config.mavenFlags ?: '-U -e'
wrapInSonarQube(config) {
sh "mvn -f ${pom} ${goals} -P${profiles} ${mavenFlags} ${properties} ${javadoc}"
}
}
} catch (InterruptedException e) {
// This can happen when the timeout() step reaches the timeout. We need to let this bubble up so
// that Jenkins can coordinate the stopping of all threads & builds that execute in parallel.
echoXWiki "XWiki build [${name}] interrupted due to timeout"
displayDebugData()
Thread.currentThread().interrupt();
// Note: Don't send email on an interrupted build.
} catch (Exception e) {
// - If this line is reached it means the build has failed (other than for failing tests) or has
// been aborted (because we told maven above to not stop on test failures!)
// - We stop the build by throwing the exception.
// - Note that withMaven() doesn't set any build result in this case but we don't need to set any
// since we're stopping the build!
// - Don't send emails for aborts! We discover aborts by checking for exit code 143.
// - Also don't send emails for process kills since it's an environment issue and not an XWiki
// source problem (this happens when the exit code is 137).
displayDebugData()
if (!e.getMessage()?.contains('exit code 143') && !e.getMessage()?.contains('exit code 137')
&& !config.skipMail)
{
sendMail('ERROR', name)
}
throw e
}
}
}
}
stage("Post Build for ${name}") {
// If the job made it to here it means the Maven build has either succeeded or some tests have failed.
// If the build has succeeded, then currentBuild.result is null (since withMaven doesn't set it in this case).
if (currentBuild.result == null) {
currentBuild.result = 'SUCCESS'
}
// Save videos generated by Docker-based tests, if any.
// Note: This can generate some not nice stack trace in the logs,
// see https://issues.jenkins-ci.org/browse/JENKINS-51913
echoXWiki "Looking for test videos in ${pwd()}"
archiveArtifacts artifacts: '**/target/**/*.flv', allowEmptyArchive: true
// Save images generated by functional tests, if any
// Note: This can generate some not nice stack trace in the logs,
// see https://issues.jenkins-ci.org/browse/JENKINS-51913
// Note: we look for screenshots only in the screenshots directory to avoid false positives such as PNG images
// that would be located in a XWiki distribution located in target/.
echoXWiki "Looking for test failure images in ${pwd()}"
archiveArtifacts artifacts: '**/target/**/screenshots/*.png', allowEmptyArchive: true
echoXWiki "Current build status after withMaven execution: ${currentBuild.result}"
// For each failed test, find if there's a screenshot for it taken by the XWiki selenium tests and if so
// embed it in the failed test's description. Also check if failing tests are flickers.
if (currentBuild.result != 'SUCCESS') {
def failingTests = getFailingTests()
echoXWiki "Failing tests: ${failingTests.collect { "${it.getClassName()}#${it.getName()}" }}"
if (!failingTests.isEmpty()) {
echoXWiki "Attaching screenshots to test result pages (if any)..."
attachScreenshotToFailingTests(failingTests)
// Check for false positives & Flickers.
echoXWiki "Checking for false positives and flickers in build results..."
def containsFalsePositivesOrOnlyFlickers = checkForFalsePositivesAndFlickers(failingTests)
// Also send a mail notification when there are not only false positives or flickering tests.
// Update 2020-10-31: Temporarily only send mails when there are failing non-functional tests to reduce
// the number of emails sent, until we fix functional tests stability. See https://bit.ly/34GTVBe
echoXWiki "Checking if email should be sent or not"
if (!containsFalsePositivesOrOnlyFlickers && !config.skipMail
&& containsNonFunctionalFailingTests(failingTests))
{
sendMail(currentBuild.result, name)
} else {
echoXWiki "No email sent even if some tests failed because they contain only flickering tests!"
echoXWiki "Considering job as successful!"
currentBuild.result = 'SUCCESS'
}
}
}
}
}
private def containsNonFunctionalFailingTests(def failingTests)
{
for (failingTest in failingTests) {
if (!failingTest.className.contains("IT")) {
return true
}
}
return false
}
private def getPOMFile(def config)
{
return config.pom ?: 'pom.xml'
}
private def getMavenSystemProperties(config, nodeName)
{
def properties = config.properties ?: ''
// Add a system property that represents the agent name so that whenever a test fails, we can display the agent
// on which it is executed in order to make it easier for debugging (it'll appear in the jenkins page for the
// failing test (see XWikiDockerExtension which prints it).
properties = "${properties} -DjenkinsAgentName=\"${nodeName}\""
// Note: We use -Dmaven.test.failure.ignore so that the maven build continues till the
// end and is not stopped by the first failing test. This allows to get more info from the
// build (see all failing tests for all modules built). Also note that the build is marked
// unstable when there are failing tests by the JUnit Archiver executed during the
// 'Post Build' stage below.
// Note: "--no-transfer-progress" is used to avoid the download progress indicators which do
// not display well in a non-interactive shell and which use a lot of console log space.
properties = "--no-transfer-progress -Dmaven.test.failure.ignore ${properties}"
// When sonar is active (sonar = true) then also pass the "sonar.branch.name" maven property so that SonarQube
// pushes the analysis to the right branch on sonarcloud. Note that we only pass it when not analyzing the main
// branch as suggested by https://community.sonarsource.com/t/clarify-the-use-of-sonar-branch-name/18872/3
def branchName = env['BRANCH_NAME']
if (config.sonar && !isMasterBranch(branchName)) {
properties = "${properties} -Dsonar.branch.name=${branchName}"
}
// Have functional tests retry twice in case of error. This is done to try to reduce the quantity of flickers.
// TODO: put back the retry when the build is in a better shape and when Surefirex 3.x doesn't fail anymore in
// running the tests for xwiki-platform-rendering-macro-python
//properties = "${properties} -Dfailsafe.rerunFailingTestsCount=2"
return properties
}
private void printConfigurationProperties(config)
{
def buffer = new StringBuilder()
config.each{ k, v -> buffer.append("[${k}] = [${v}]\n") }
echoXWiki "Passed configuration properties:\n${buffer.toString()}"
}
private def getMavenProfiles(config, env)
{
def profiles = config.profiles ?: 'quality,legacy,integration-tests,jetty,hsqldb,firefox'
// If we're on a node supporting docker, also build the docker-based tests (i.e. for the default configuration)
if (env.NODE_LABELS.split().contains('docker')) {
profiles = "${profiles},docker"
}
return profiles
}
private def wrapInXvnc(config, closure)
{
def isXvncEnabled = config.xvnc == null ? true : config.xvnc
if (isXvncEnabled) {
wrap([$class: 'Xvnc']) {
closure()
}
} else {
echoXWiki "Xvnc disabled, building without it!"
closure()
}
}
private def wrapInSonarQube(config, closure)
{
if (config.sonar) {
withSonarQubeEnv('sonar') {
// Note: SonarQube taskId is automatically attached to the pipeline context
closure()
}
// Check the SonarQube quality gates
// Note: the timeout is just in case something goes wrong
timeout(time: 1, unit: 'HOURS') {
// Reuse taskId previously collected by withSonarQubeEnv
def qg = waitForQualityGate()
if (qg.status != 'OK') {
error "Pipeline aborted due to quality gate failure: ${qg.status}"
}
}
} else {
closure()
}
}
private def computeMavenGoals(config)
{
def goals = config.goals
if (!goals) {
// Use "deploy" goal for the master branch and the "stable-*" branches only and "install" for all branches.
// This is to avoid having branches with the same version in pom.xml, polluting the maven snapshot repo,
// overwriting one another.
def branchName = env['BRANCH_NAME']
if (branchName != null && (isMasterBranch(branchName) || isStableBranch(branchName))) {
goals = "deploy"
} else {
goals = "install"
}
goals = "clean ${goals}"
}
return goals
}
private def isStableBranch(def branchName)
{
return branchName.startsWith('stable-')
}
/**
* Create a FilePath instance that points either to a file on the master node or a file on a remote agent node.
*/
private def createFilePath(String path)
{
if (env['NODE_NAME'] == null) {
error "envvar NODE_NAME is not set, probably not inside an node {} or running an older version of Jenkins!"
} else if (env['NODE_NAME'].equals("master")) {
return new FilePath(new File(path))
} else {
return new FilePath(Jenkins.getInstance().getComputer(env['NODE_NAME']).getChannel(), path)
}
}
/**
* Attach the screenshot of failing XWiki Selenium tests to failed test descriptions.
* The screenshot is preserved after the workspace gets cleared by a new build.
*
* To make this script works the following needs to be setup on the Jenkins instance:
* <ul>
* <li>Install the <a href="http://wiki.jenkins-ci.org/display/JENKINS/Groovy+Postbuild+Plugin">Groovy Postbuild
* plugin</a>. This exposes the "manager" variable needed by the script.</li>
* <li>Add the required security exceptions to http://<jenkins server ip>/scriptApproval/ if need be.</li>
* <li>Install the <a href="https://wiki.jenkins-ci.org/display/JENKINS/PegDown+Formatter+Plugin">Pegdown Formatter
* plugin</a> and set the description syntax to be Pegdown in the Global Security configuration
* (http://<jenkins server ip>/configureSecurity).</li>
* </ul>
*/
private def attachScreenshotToFailingTests(def failingTests)
{
// Go through each failed test in the current build.
for (def failedTest : failingTests) {
// Compute the test's screenshot file name.
def testClass = failedTest.className
def testName = failedTest.name
def targetDirectory = computeTargetDirectoryForTest(failedTest)
if (!targetDirectory) {
// We couldn't compute the target directory, move to the next test!
echoXWiki "Failed to find target directory for test [${testClass}#${testName}]"
continue
}
def imageAbsolutePath = findScreenshotFile(failedTest, targetDirectory)
// If a screenshot exists...
if (imageAbsolutePath) {
echoXWiki "Attaching screenshot to description: [${imageAbsolutePath}]"
// Build a base64 string of the image's content.
def imageDataStream = imageAbsolutePath.read()
byte[] imageData = IOUtils.toByteArray(imageDataStream)
def imageDataString = "data:image/png;base64," + DatatypeConverter.printBase64Binary(imageData)
def testResultAction = failedTest.getParentAction()
// Build a description HTML to be set for the failing test that includes the image in Data URI format.
def imgText = """<img style="width: 800px" src="${imageDataString}" />"""
def description = """<h3>Screenshot</h3><a href="${imageDataString}">${imgText}</a>"""
// Only modify the description if the test page hasn't been modified already
if (!description.equals(testResultAction.getDescription(failedTest))) {
// Set the description to the failing test and save it to disk.
testResultAction.setDescription(failedTest, description)
// Clear potentially problematic non-serializable object reference, after we've used it.
// See https://github.com/jenkinsci/pipeline-plugin/blob/master/TUTORIAL.md#serializing-local-variables
testResultAction = null
saveCurrentBuildChanges()
}
} else {
echoXWiki "No screenshot found for test [${testClass}#${testName}] on ${NODE_NAME}"
sh script: "ls -alg ${targetDirectory}", returnStatus: true
}
}
}
private def findScreenshotFile(def failedTest, def targetDirectory)
{
// The screenshot can have several possible file names and locations, we check all.
// Selenium 1 test screenshots.
def imageAbsolutePath1 = new FilePath(targetDirectory, "selenium-screenshots")
// Selenium 2 test screenshots.
def imageAbsolutePath2 = new FilePath(targetDirectory, "screenshots")
// If screenshotDirectory system property is not defined we save screenshots in the tmp dir so we must also
// support this.
def imageAbsolutePath3 = createFilePath(System.getProperty("java.io.tmpdir"))
// Determine which one exists, if any.
return findScreenshotFileForPattern(imageAbsolutePath1, failedTest) ?:
findScreenshotFileForPattern(imageAbsolutePath2, failedTest) ?:
findScreenshotFileForPattern(imageAbsolutePath3, failedTest)
}
private def findScreenshotFileForPattern(def directoryFilePath, def failedTest)
{
def files = [] as Set
// Remove the serialized parameters from the test name FTM since we output failing test image names without it.
// The best fix would be to modify the Docker-based test framework to add the parameters but I don't know how to do
// that ATM (i.e. what JUnit API to call to get it).
def normalizedTestName = failedTest.name
def pos = normalizedTestName.indexOf("{")
if (pos > -1) {
normalizedTestName = normalizedTestName.substring(0, pos)
}
if (directoryFilePath.exists()) {
files.addAll(directoryFilePath.list("*${failedTest.className}-${normalizedTestName}*.png"))
files.addAll(directoryFilePath.list("*${failedTest.simpleName}-${normalizedTestName}*.png"))
}
if (files.size() > 1) {
echoXWiki "Found several matching screenshots which should not happen (something needs to be fixed): ${files}"
return null
} else if (files.size() == 1) {
return files[0]
} else {
echoXWiki "No matching screenshot found for [*${failedTest.className}-${normalizedTestName}*.png] or [*${failedTest.simpleName}-${normalizedTestName}*.png] inside [${directoryFilePath.remote}]"
return null
}
}
private def computeTargetDirectoryForTest(def caseResult)
{
// A CaseResult has a SuiteResult as parent which holds the JUnit XML file path, from which we can infer the
// target directory for the passed test.
// Example of value for suiteResultFile (it's a String):
// /Users/vmassol/.jenkins/workspace/blog/application-blog-test/application-blog-test-tests/target/
// surefire-reports/TEST-org.xwiki.blog.test.ui.AllTests.xml
def suiteResultFile = caseResult.getSuiteResult().getFile()
if (suiteResultFile == null) {
return
}
// Compute the screenshot's location on the build agent.
// Example of target folder path:
// /Users/vmassol/.jenkins/workspace/blog/application-blog-test/application-blog-test-tests/target
def targetDirectory = createFilePath(suiteResultFile).getParent().getParent()
// When executing docker-based tests as part of the main build (ie with the default configuration), we use a
// subdirectory inside the target directory, and it's in it that we save the screenshots and videos. Thus we need
// to test for that directory's existence and if it exists, return it.
if (targetDirectory.exists()) {
def subDirectory = targetDirectory.child("hsqldb_embedded-default-default-jetty_standalone-default-firefox")
if (subDirectory.exists()) {
targetDirectory = subDirectory
}
}
return targetDirectory;
}
/**
* Check for false positives for known cases of failures not related to code + check for test flickers.
*
* @return true if the build has false positives or if there are only flickering tests
*/
private def checkForFalsePositivesAndFlickers(def failingTests)
{
// Step 1: Check for false positives
def containsFalsePositives = checkForFalsePositives()
// Step 2: Check for flickers
def containsOnlyFlickers = checkForFlickers(failingTests)
return containsFalsePositives || containsOnlyFlickers
}
/**
* Check for test flickers, and modify test result descriptions for tests that are identified as flicker. A test is
* a flicker if there's a JIRA issue having the "Flickering Test" custom field containing the FQN of the test in the
* format "<java package name>#<test name>".
*
* @return true if the failing tests only contain flickering tests
*/
private def checkForFlickers(def failingTests)
{
def knownFlickers = getKnownFlickeringTests()
echoXWiki "Known flickering tests: ${knownFlickers}"
// For each failed test, check if it's in the known flicker list.
Set foundFlickers = []
boolean containsOnlyFlickers = true
boolean isModified = false
failingTests.each() { testResult ->
// Construct a normalized test name made of <test class name>#<method name>
// Note: The call to toString() is important to get a String and not a GString so that contains() will work
// (since otherwise equals() will fail between a String and a GString)
// TODO: Put it back when we understand why it makes the CI fail with:
// groovy.lang.MissingMethodException: No signature of method: java.lang.String.containsKey() is
// applicable for argument types: (java.lang.String) values: ...
//def normalizedTestName = normalizeTestName(testResult.name)
//def testName = "${testResult.className}#${normalizedTestName}".toString()
def testName = "${testResult.className}#${testResult.name}".toString()
echoXWiki "Analyzing test [${testName}] for flicker ..."
if (knownFlickers.containsKey(testName)) {
// Add the information that the test is a flicker to the test's description. Only display this
// once (a Jenkinsfile can contain several builds and thus this code can be called several times
// for the same tests, as the failing tests passed are the failing tests for the whole job, see
// getFailingTests(). We haven't found a way to get the failing tests only for the current withMaven
// execution).
def flickeringText =
"<h3 style='color:red'>This is a <a href='${knownFlickers.get(testName)}'>flickering</a> test</h3>"
if (testResult.getDescription() == null || !testResult.getDescription().contains(flickeringText)) {
testResult.setDescription("${flickeringText}${testResult.getDescription() ?: ''}")
isModified = true
} else {
// For debugging
echoXWiki "Flicker [${testName}] - Description = [${testResult.getDescription()}]"
}
echoXWiki " [${testName}] is a flicker!"
foundFlickers.add(testName)
} else {
echoXWiki " [${testName}] is not a flicker"
// This is a real failing test, thus we'll need to send the notification email...
containsOnlyFlickers = false
}
}
if (foundFlickers) {
def badgeText = 'Contains some flickering tests'
def badgeFound = isBadgeFound(badgeText)
if (!badgeFound) {
manager.addWarningBadge(badgeText)
}
// Replace the existing summary with the accrued list of flickers found
manager.removeSummaries()
def summary = manager.createSummary("warning.gif")
summary.appendText("Flickering tests<ul>", false, false, false, 'red')
foundFlickers.each() {
summary.appendText("<li><a href='${knownFlickers.get(it)}'>${it}</a></li>", false, false, false, 'red')
}
summary.appendText("</ul>", false, false, false, 'red')
isModified = true
}
if (isModified) {
// Persist changes
saveCurrentBuildChanges()
}
return containsOnlyFlickers
}
/**
* @return all known flickering tests from JIRA in the format
* {@code org.xwiki.test.ui.repository.RepositoryTest#validateAllFeatures}
*/
@NonCPS
private def getKnownFlickeringTests()
{
def knownFlickers = [:]
def url = "https://jira.xwiki.org/sr/jira.issueviews:searchrequest-xml/temp/SearchRequest.xml?".concat(
"jqlQuery=%22Flickering%20Test%22%20is%20not%20empty%20and%20resolution%20=%20Unresolved")
def root = new XmlSlurper().parseText(url.toURL().text)
// Note: slurper nodes are not serializable, hence the @NonCPS annotation above.
def packageName = ''
root.channel.item.customfields.customfield.each() { customfield ->
if (customfield.customfieldname == 'Flickering Test') {
def trimSpaces = {
def trimmedIt = it.trim()
// When a leading space is adding in jira, the resulting XML value we get for it is "&nbsp;".
trimmedIt.startsWith('&nbsp;') ? trimmedIt - '&nbsp;' : trimmedIt
}
customfield.customfieldvalues.customfieldvalue.text().split('\\|').each() {
def trimmedValue = trimSpaces(it)
// Check if a package is specified and if not use the previously found package name
// This is an optimization to make it shorter to specify several tests in the same test class.
// e.g.: "org.xwiki.test.ui.extension.ExtensionTest#testUpgrade,testUninstall"
def fullName
int pos = trimmedValue.indexOf('#')
if (pos > -1) {
packageName = trimmedValue.substring(0, pos)
fullName = trimmedValue
} else {
fullName = "${packageName}#${trimmedValue}"
}
// Remove the part between "{" and "}" since we don't use test methods which differ only by their
// parameters, and removing this make the jira issues more stable against refactorings. Also prevents
// user mistakes.
// TODO: Put it back when we understand why it makes the CI fail with:
// groovy.lang.MissingMethodException: No signature of method: java.lang.String.containsKey() is
// applicable for argument types: (java.lang.String) values: ...
//fullName = normalizeTestName(fullName)
knownFlickers.put(fullName, customfield.parent().parent().link.text())
}
}
}
return knownFlickers
}
private def normalizeTestName(value)
{
def newValue
def pos1 = value.indexOf('{')
if (pos1 > -1) {
def pos2 = value.lastIndexOf('}', pos1)
if (pos2 > -1) {
newValue = new StringBuilder().append(value, 0, pos1).append(value, pos2).toString()
} else {
// Remove till end of string
newValue = value.substring(0, pos1)
}
} else {
newValue = value
}
return newValue
}
/**
* @return the failing tests for the current build as a list of {@code hudson.tasks.junit.CaseResult} objects.
*/
// TODO: Note that this is currently not workig as it returns all failing tests from all maven executions so far.
// See also https://issues.jenkins.io/browse/JENKINS-49339
// currentBuild.rawBuild is non-serializable which is why we need the @NonCPS annotation.
// Search for "rawBuild" on https://ci.xwiki.org/pipeline-syntax/globals#currentBuild
// Otherwise we get: Caused: java.io.NotSerializableException: org.jenkinsci.plugins.workflow.job.WorkflowRun
@NonCPS
private def getFailingTests()
{
def failingTests
def currentRun = currentBuild.rawBuild
AbstractTestResultAction testResultAction = currentRun.getAction(AbstractTestResultAction.class)
if (testResultAction != null) {
// Note: getResultInRun() returns a https://javadoc.jenkins.io/plugin/junit/hudson/tasks/test/TestResult.html
failingTests = testResultAction.getResult().getResultInRun(currentBuild.rawBuild).getFailedTests()
} else {
// No tests were run in this build, nothing left to do.
failingTests = []
}
return failingTests
}