[CI] Add pipeline library unit tests (#68556) (#68910)

This commit is contained in:
Brian Seeders 2020-06-15 16:37:00 -04:00 committed by GitHub
parent 6226aeac19
commit e9627b09de
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 639 additions and 4 deletions

View file

@ -0,0 +1,8 @@
# Kibana Jenkins Pipeline Library
## Running tests
```bash
cd .ci/pipeline-library
./gradlew test
```

View file

@ -0,0 +1,45 @@
plugins {
id 'groovy'
id 'idea'
}
group = 'co.elastic.kibana.pipeline'
version = '0.0.1'
sourceCompatibility = 1.8
targetCompatibility = 1.8
repositories {
maven { url 'https://repo.jenkins-ci.org/releases/' }
maven { url 'https://repo.maven.apache.org/maven2' }
}
dependencies {
implementation 'org.codehaus.groovy:groovy-all:2.4.12'
implementation 'org.jenkins-ci.main:jenkins-core:2.23'
implementation 'org.jenkins-ci.plugins.workflow:workflow-step-api:2.19@jar'
testImplementation 'com.lesfurets:jenkins-pipeline-unit:1.4'
testImplementation 'junit:junit:4.12'
testImplementation 'org.assertj:assertj-core:3.15+' // Temporary https://github.com/jenkinsci/JenkinsPipelineUnit/issues/209
}
sourceSets {
main {
groovy {
srcDirs = ['vars']
}
}
test {
groovy {
srcDirs = ['src/test']
}
}
}
test {
testLogging {
events 'passed', 'skipped', 'failed'
exceptionFormat = 'full'
}
}

Binary file not shown.

View file

@ -0,0 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-6.4.1-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

185
.ci/pipeline-library/gradlew vendored Executable file
View file

@ -0,0 +1,185 @@
#!/usr/bin/env sh
#
# Copyright 2015 the original author or authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
##############################################################################
##
## Gradle start up script for UN*X
##
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
PRG="$0"
# Need this for relative symlinks.
while [ -h "$PRG" ] ; do
ls=`ls -ld "$PRG"`
link=`expr "$ls" : '.*-> \(.*\)$'`
if expr "$link" : '/.*' > /dev/null; then
PRG="$link"
else
PRG=`dirname "$PRG"`"/$link"
fi
done
SAVED="`pwd`"
cd "`dirname \"$PRG\"`/" >/dev/null
APP_HOME="`pwd -P`"
cd "$SAVED" >/dev/null
APP_NAME="Gradle"
APP_BASE_NAME=`basename "$0"`
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD="maximum"
warn () {
echo "$*"
}
die () {
echo
echo "$*"
echo
exit 1
}
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "`uname`" in
CYGWIN* )
cygwin=true
;;
Darwin* )
darwin=true
;;
MINGW* )
msys=true
;;
NONSTOP* )
nonstop=true
;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD="$JAVA_HOME/jre/sh/java"
else
JAVACMD="$JAVA_HOME/bin/java"
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD="java"
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
# Increase the maximum file descriptors if we can.
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
MAX_FD_LIMIT=`ulimit -H -n`
if [ $? -eq 0 ] ; then
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
MAX_FD="$MAX_FD_LIMIT"
fi
ulimit -n $MAX_FD
if [ $? -ne 0 ] ; then
warn "Could not set maximum file descriptor limit: $MAX_FD"
fi
else
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
fi
fi
# For Darwin, add options to specify how the application appears in the dock
if $darwin; then
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
fi
# For Cygwin or MSYS, switch paths to Windows format before running java
if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
JAVACMD=`cygpath --unix "$JAVACMD"`
# We build the pattern for arguments to be converted via cygpath
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
SEP=""
for dir in $ROOTDIRSRAW ; do
ROOTDIRS="$ROOTDIRS$SEP$dir"
SEP="|"
done
OURCYGPATTERN="(^($ROOTDIRS))"
# Add a user-defined pattern to the cygpath arguments
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
fi
# Now convert the arguments - kludge to limit ourselves to /bin/sh
i=0
for arg in "$@" ; do
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
else
eval `echo args$i`="\"$arg\""
fi
i=`expr $i + 1`
done
case $i in
0) set -- ;;
1) set -- "$args0" ;;
2) set -- "$args0" "$args1" ;;
3) set -- "$args0" "$args1" "$args2" ;;
4) set -- "$args0" "$args1" "$args2" "$args3" ;;
5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
esac
fi
# Escape application args
save () {
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
echo " "
}
APP_ARGS=`save "$@"`
# Collect all arguments for the java command, following the shell quoting and substitution rules
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
exec "$JAVACMD" "$@"

104
.ci/pipeline-library/gradlew.bat vendored Normal file
View file

@ -0,0 +1,104 @@
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@if "%DEBUG%" == "" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%" == "" set DIRNAME=.
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if "%ERRORLEVEL%" == "0" goto init
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto init
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:init
@rem Get command-line arguments, handling Windows variants
if not "%OS%" == "Windows_NT" goto win9xME_args
:win9xME_args
@rem Slurp the command line arguments.
set CMD_LINE_ARGS=
set _SKIP=2
:win9xME_args_slurp
if "x%~1" == "x" goto execute
set CMD_LINE_ARGS=%*
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
:end
@rem End local scope for the variables with windows NT shell
if "%ERRORLEVEL%"=="0" goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
exit /b 1
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega

View file

@ -0,0 +1,106 @@
import com.lesfurets.jenkins.unit.*
import org.junit.Before
class KibanaBasePipelineTest extends BasePipelineTest {
Map env = [:]
Map params = [:]
public def Mocks = [
TEST_FAILURE_URL: 'https://localhost/',
TEST_FAILURE_NAME: 'Kibana Pipeline / kibana-xpack-agent / Chrome X-Pack UI Functional Tests.x-pack/test/functional/apps/fake/test·ts.Fake test <Component> should & pass &',
]
@Before
void setUp() {
super.setUp()
env.BRANCH_NAME = 'master'
env.BUILD_ID = '1'
env.BUILD_DISPLAY_NAME = "#${env.BUILD_ID}"
env.JENKINS_URL = 'http://jenkins.localhost:8080'
env.BUILD_URL = "${env.JENKINS_URL}/job/elastic+kibana+${env.BRANCH_NAME}/${env.BUILD_ID}/"
env.JOB_BASE_NAME = "elastic / kibana # ${env.BRANCH_NAME}"
env.JOB_NAME = env.JOB_BASE_NAME
env.WORKSPACE = 'WS'
props([
buildUtils: [
getBuildStatus: { 'SUCCESS' },
printStacktrace: { ex -> print ex },
],
jenkinsApi: [ getFailedSteps: { [] } ],
testUtils: [ getFailures: { [] } ],
])
vars([
env: env,
params: params,
])
// Some wrappers that can just be mocked to immediately call the closure passed in
[
'catchError',
'catchErrors',
'timestamps',
'withGithubCredentials',
].each {
helper.registerAllowedMethod(it, [Closure.class], null)
}
}
void props(Map properties) {
properties.each {
binding.setProperty(it.key, it.value)
}
}
void prop(String propertyName, Object propertyValue) {
binding.setProperty(propertyName, propertyValue)
}
void vars(Map variables) {
variables.each {
binding.setVariable(it.key, it.value)
}
}
void var(String variableName, Object variableValue) {
binding.setVariable(variableName, variableValue)
}
def fnMock(String name) {
return helper.callStack.find { it.methodName == name }
}
void mockFailureBuild() {
props([
buildUtils: [
getBuildStatus: { 'FAILURE' },
printStacktrace: { ex -> print ex },
],
jenkinsApi: [ getFailedSteps: { [
[
displayName: 'Check out from version control',
logs: 'http://jenkins.localhost:8080',
],
[
displayName: 'Execute test task',
logs: 'http://jenkins.localhost:8080',
],
] } ],
testUtils: [
getFailures: {
return [
[
url: Mocks.TEST_FAILURE_URL,
fullDisplayName: Mocks.TEST_FAILURE_NAME,
]
]
},
],
])
}
}

View file

@ -0,0 +1,87 @@
import org.junit.*
import static groovy.test.GroovyAssert.*
class PrChangesTest extends KibanaBasePipelineTest {
def prChanges
@Before
void setUp() {
super.setUp()
env.ghprbPullId = '1'
props([
githubPr: [
isPr: { true },
],
])
prChanges = loadScript("vars/prChanges.groovy")
}
@Test
void 'areChangesSkippable() with no changes'() {
props([
githubPrs: [
getChanges: { [] },
],
])
assertTrue(prChanges.areChangesSkippable())
}
@Test
void 'areChangesSkippable() with skippable changes'() {
props([
githubPrs: [
getChanges: { [
[filename: 'docs/test/a-fake-doc.asciidoc'],
[filename: 'README.md'],
] },
],
])
assertTrue(prChanges.areChangesSkippable())
}
@Test
void 'areChangesSkippable() with skippable renames'() {
props([
githubPrs: [
getChanges: { [
[ filename: 'docs/test/a-fake-doc.asciidoc', previousFilename: 'docs/test/a-different-fake-doc.asciidoc' ],
[ filename: 'README.md', previousFilename: 'README-old.md' ],
] },
],
])
assertTrue(prChanges.areChangesSkippable())
}
@Test
void 'areChangesSkippable() with unskippable changes'() {
props([
githubPrs: [
getChanges: { [
[filename: 'src/core/index.ts'],
] },
],
])
assertFalse(prChanges.areChangesSkippable())
}
@Test
void 'areChangesSkippable() with skippable and unskippable changes'() {
props([
githubPrs: [
getChanges: { [
[filename: 'README.md'],
[filename: 'src/core/index.ts'],
] },
],
])
assertFalse(prChanges.areChangesSkippable())
}
}

View file

@ -0,0 +1,62 @@
import org.junit.*
import static groovy.test.GroovyAssert.*
class SlackNotificationsTest extends KibanaBasePipelineTest {
def slackNotifications
@Before
void setUp() {
super.setUp()
helper.registerAllowedMethod('slackSend', [Map.class], null)
slackNotifications = loadScript('vars/slackNotifications.groovy')
}
@Test
void 'getTestFailures() should properly format failure steps'() {
mockFailureBuild()
def failureMessage = slackNotifications.getTestFailures()
assertEquals(
"*Test Failures*\n• <${Mocks.TEST_FAILURE_URL}|x-pack/test/functional/apps/fake/test·ts.Fake test &lt;Component&gt; should &amp; pass &amp;>",
failureMessage
)
}
@Test
void 'sendFailedBuild() should call slackSend() with message'() {
mockFailureBuild()
slackNotifications.sendFailedBuild()
def args = fnMock('slackSend').args[0]
def expected = [
channel: '#kibana-operations-alerts',
username: 'Kibana Operations',
iconEmoji: ':jenkins:',
color: 'danger',
message: ':broken_heart: elastic / kibana # master #1',
]
expected.each {
assertEquals(it.value.toString(), args[it.key].toString())
}
assertEquals(
":broken_heart: *<http://jenkins.localhost:8080/job/elastic+kibana+master/1/|elastic / kibana # master #1>*",
args.blocks[0].text.text.toString()
)
assertEquals(
"*Failed Steps*\n• <http://jenkins.localhost:8080|Execute test task>",
args.blocks[1].text.text.toString()
)
assertEquals(
"*Test Failures*\n• <https://localhost/|x-pack/test/functional/apps/fake/test·ts.Fake test &lt;Component&gt; should &amp; pass &amp;>",
args.blocks[2].text.text.toString()
)
}
}

1
.ci/pipeline-library/vars Symbolic link
View file

@ -0,0 +1 @@
../../vars

2
.gitignore vendored
View file

@ -46,6 +46,8 @@ package-lock.json
npm-debug.log*
.tern-project
.nyc_output
.ci/pipeline-library/build/
.gradle
# apm plugin
/x-pack/plugins/apm/tsconfig.json

View file

@ -46,6 +46,7 @@ export const IGNORE_FILE_GLOBS = [
'**/Jenkinsfile*',
'Dockerfile*',
'vars/*',
'.ci/pipeline-library/**/*',
// Files in this directory must match a pre-determined name in some cases.
'x-pack/plugins/canvas/.storybook/*',

View file

@ -204,6 +204,8 @@ def call(Map params = [:], Closure closure) {
timestamps {
ansiColor('xterm') {
if (config.checkPrChanges && githubPr.isPr()) {
pipelineLibraryTests()
print "Checking PR for changes to determine if CI needs to be run..."
if (prChanges.areChangesSkippable()) {
@ -218,5 +220,15 @@ def call(Map params = [:], Closure closure) {
}
}
def pipelineLibraryTests() {
whenChanged(['vars/', '.ci/pipeline-library/']) {
workers.base(size: 'flyweight', bootstrapped: false, ramDisk: false) {
dir('.ci/pipeline-library') {
sh './gradlew test'
}
}
}
}
return this

View file

@ -1,6 +1,6 @@
import groovy.transform.Field
public static @Field PR_CHANGES_CACHE = null
public static @Field PR_CHANGES_CACHE = []
def getSkippablePaths() {
return [
@ -8,6 +8,7 @@ def getSkippablePaths() {
/^rfcs\//,
/^.ci\/.+\.yml$/,
/^.ci\/es-snapshots\//,
/^.ci\/pipeline-library\//,
/^\.github\//,
/\.md$/,
]
@ -42,7 +43,10 @@ def areChangesSkippable() {
def getChanges() {
if (!PR_CHANGES_CACHE && env.ghprbPullId) {
withGithubCredentials {
PR_CHANGES_CACHE = githubPrs.getChanges(env.ghprbPullId)
def changes = githubPrs.getChanges(env.ghprbPullId)
if (changes) {
PR_CHANGES_CACHE.addAll(changes)
}
}
}

View file

@ -62,7 +62,17 @@ def getTestFailures() {
def messages = []
messages << "*Test Failures*"
def list = failures.collect { "• <${it.url}|${it.fullDisplayName.split(/\./, 2)[-1]}>" }.join("\n")
def list = failures.collect {
def name = it
.fullDisplayName
.split(/\./, 2)[-1]
// Only the following three characters need to be escaped for link text, per Slack's docs
.replaceAll('&', '&amp;')
.replaceAll('<', '&lt;')
.replaceAll('>', '&gt;')
return "• <${it.url}|${name}>"
}.join("\n")
return "*Test Failures*\n${list}"
}
@ -100,6 +110,7 @@ def sendFailedBuild(Map params = [:]) {
] + params
def title = "${getStatusIcon()} ${config.title}"
def message = "${getStatusIcon()} ${config.message}"
def blocks = [markdownBlock(title)]
getFailedBuildBlocks().each { blocks << it }
@ -111,7 +122,7 @@ def sendFailedBuild(Map params = [:]) {
username: config.username,
iconEmoji: config.icon,
color: config.color,
message: config.message,
message: message,
blocks: blocks
)
}

View file

@ -3,6 +3,8 @@
def label(size) {
switch(size) {
case 'flyweight':
return 'flyweight'
case 's':
return 'linux && immutable'
case 's-highmem':