diff --git a/atlas-jenkins-pipeline-test/.gitattributes b/atlas-jenkins-pipeline-test/.gitattributes new file mode 100644 index 00000000..00a51aff --- /dev/null +++ b/atlas-jenkins-pipeline-test/.gitattributes @@ -0,0 +1,6 @@ +# +# https://help.github.com/articles/dealing-with-line-endings/ +# +# These are explicitly windows files and should use crlf +*.bat text eol=crlf + diff --git a/atlas-jenkins-pipeline-test/.gitignore b/atlas-jenkins-pipeline-test/.gitignore new file mode 100644 index 00000000..1b6985c0 --- /dev/null +++ b/atlas-jenkins-pipeline-test/.gitignore @@ -0,0 +1,5 @@ +# Ignore Gradle project-specific cache directory +.gradle + +# Ignore Gradle build output directory +build diff --git a/atlas-jenkins-pipeline-test/build.gradle b/atlas-jenkins-pipeline-test/build.gradle new file mode 100644 index 00000000..ae05bd07 --- /dev/null +++ b/atlas-jenkins-pipeline-test/build.gradle @@ -0,0 +1,41 @@ +/* + * This file was generated by the Gradle 'init' task. + * + * This generated file contains a sample Groovy library project to get you started. + * For more details take a look at the 'Building Java & JVM projects' chapter in the Gradle + * User Manual available at https://docs.gradle.org/6.9.1/userguide/building_java_projects.html + */ + +plugins { + id 'groovy' + id 'java-library' +} +java.sourceCompatibility = JavaVersion.VERSION_1_8 + +repositories { + mavenCentral() + maven { + url = uri('https://repo.jenkins-ci.org/releases/') + } + maven { + url = uri("http://repo.jenkins-ci.org/public") + } +} + +dependencies { + // Use the latest Groovy version for building this library + api "com.lesfurets:jenkins-pipeline-unit:1.9" + implementation 'org.codehaus.groovy:groovy-all:2.4.+' + implementation "org.spockframework:spock-core:1.3-groovy-2.4" + implementation group: "org.jenkins-ci.plugins", name: "script-security", version:"1158.v7c1b_73a_69a_08", ext: "jar" + implementation group: "org.kohsuke", name: "groovy-sandbox", version:"1.27", ext: "jar" + + //needed jenkins stuff used implicitly + implementation("org.jenkins-ci.main:jenkins-core:2.332.2") { + exclude module: "groovy-all" + } + implementation "org.jenkins-ci.main:jenkins-test-harness:2.71" + implementation 'com.github.ben-manes.caffeine:caffeine:2.9.2' + implementation 'org.powermock:powermock-classloading-xstream:2.0.9' + +} diff --git a/atlas-jenkins-pipeline-test/gradle/wrapper/gradle-wrapper.jar b/atlas-jenkins-pipeline-test/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 00000000..e708b1c0 Binary files /dev/null and b/atlas-jenkins-pipeline-test/gradle/wrapper/gradle-wrapper.jar differ diff --git a/atlas-jenkins-pipeline-test/gradle/wrapper/gradle-wrapper.properties b/atlas-jenkins-pipeline-test/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 00000000..3ab0b725 --- /dev/null +++ b/atlas-jenkins-pipeline-test/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-6.9.1-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/atlas-jenkins-pipeline-test/gradlew b/atlas-jenkins-pipeline-test/gradlew new file mode 100755 index 00000000..4f906e0c --- /dev/null +++ b/atlas-jenkins-pipeline-test/gradlew @@ -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" "$@" diff --git a/atlas-jenkins-pipeline-test/gradlew.bat b/atlas-jenkins-pipeline-test/gradlew.bat new file mode 100644 index 00000000..107acd32 --- /dev/null +++ b/atlas-jenkins-pipeline-test/gradlew.bat @@ -0,0 +1,89 @@ +@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 execute + +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 execute + +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 + +: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 %* + +: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 diff --git a/atlas-jenkins-pipeline-test/settings.gradle b/atlas-jenkins-pipeline-test/settings.gradle new file mode 100644 index 00000000..927bd7a4 --- /dev/null +++ b/atlas-jenkins-pipeline-test/settings.gradle @@ -0,0 +1,10 @@ +/* + * This file was generated by the Gradle 'init' task. + * + * The settings file is used to specify which projects to include in your build. + * + * Detailed information about configuring a multi-project build in Gradle can be found + * in the user manual at https://docs.gradle.org/6.9.1/userguide/multi_project_builds.html + */ + +rootProject.name = 'atlas-jenkins-pipeline-test' diff --git a/atlas-jenkins-pipeline-test/src/main/groovy/net/wooga/jenkins/pipeline/test/exceptions/JenkinsError.groovy b/atlas-jenkins-pipeline-test/src/main/groovy/net/wooga/jenkins/pipeline/test/exceptions/JenkinsError.groovy new file mode 100644 index 00000000..0b0185a6 --- /dev/null +++ b/atlas-jenkins-pipeline-test/src/main/groovy/net/wooga/jenkins/pipeline/test/exceptions/JenkinsError.groovy @@ -0,0 +1,8 @@ +package net.wooga.jenkins.pipeline.test.exceptions + +class JenkinsError extends RuntimeException { + + JenkinsError(String message) { + super(message) + } +} diff --git a/atlas-jenkins-pipeline-test/src/main/groovy/net/wooga/jenkins/pipeline/test/sandbox/GroovyClassLoaderWhitelist.groovy b/atlas-jenkins-pipeline-test/src/main/groovy/net/wooga/jenkins/pipeline/test/sandbox/GroovyClassLoaderWhitelist.groovy new file mode 100644 index 00000000..3390c4bb --- /dev/null +++ b/atlas-jenkins-pipeline-test/src/main/groovy/net/wooga/jenkins/pipeline/test/sandbox/GroovyClassLoaderWhitelist.groovy @@ -0,0 +1,59 @@ +package net.wooga.jenkins.pipeline.test.sandbox + +import org.jenkinsci.plugins.scriptsecurity.sandbox.Whitelist + +import java.lang.reflect.Constructor +import java.lang.reflect.Field +import java.lang.reflect.Method + +//Based on ClassLoaderWhitelist from jenkins' security plugin +class GroovyClassLoaderWhitelist extends Whitelist { + + private final ClassLoader scriptLoader; + + public GroovyClassLoaderWhitelist(ClassLoader scriptLoader) { + this.scriptLoader = scriptLoader; + } + + private boolean permitClassLoader(ClassLoader classLoader) { + //Groovy uses these inner loaders to load scripts tru GSE. + // This isn't a problem in jenkins itself for some reason, but it is in our tests, + // so we have to find the nearest non-innerloader parent. + if(classLoader!= null && classLoader instanceof GroovyClassLoader.InnerLoader) { + return permitClassLoader(classLoader.parent) + } + return classLoader == scriptLoader + } + + private boolean permit(Class declaringClass) { + return permitClassLoader(declaringClass.classLoader) + } + + @Override boolean permitsMethod(Method method, Object receiver, Object[] args) { + return permit(method.getDeclaringClass()); + } + + @Override boolean permitsConstructor(Constructor constructor, Object[] args) { + return permit(constructor.getDeclaringClass()); + } + + @Override boolean permitsStaticMethod(Method method, Object[] args) { + return permit(method.getDeclaringClass()); + } + + @Override boolean permitsFieldGet(Field field, Object receiver) { + return permit(field.getDeclaringClass()); + } + + @Override boolean permitsFieldSet(Field field, Object receiver, Object value) { + return permit(field.getDeclaringClass()); + } + + @Override boolean permitsStaticFieldGet(Field field) { + return permit(field.getDeclaringClass()); + } + + @Override boolean permitsStaticFieldSet(Field field, Object value) { + return permit(field.getDeclaringClass()); + } +} diff --git a/test/groovy/tools/sandbox/MethodSignatureWhitelist.groovy b/atlas-jenkins-pipeline-test/src/main/groovy/net/wooga/jenkins/pipeline/test/sandbox/MethodSignatureWhitelist.groovy similarity index 95% rename from test/groovy/tools/sandbox/MethodSignatureWhitelist.groovy rename to atlas-jenkins-pipeline-test/src/main/groovy/net/wooga/jenkins/pipeline/test/sandbox/MethodSignatureWhitelist.groovy index a192cedc..5bb04d7b 100644 --- a/test/groovy/tools/sandbox/MethodSignatureWhitelist.groovy +++ b/atlas-jenkins-pipeline-test/src/main/groovy/net/wooga/jenkins/pipeline/test/sandbox/MethodSignatureWhitelist.groovy @@ -1,4 +1,4 @@ -package tools.sandbox +package net.wooga.jenkins.pipeline.test.sandbox import com.lesfurets.jenkins.unit.MethodSignature import org.jenkinsci.plugins.scriptsecurity.sandbox.whitelists.AbstractWhitelist diff --git a/test/groovy/tools/sandbox/PackageWhitelist.groovy b/atlas-jenkins-pipeline-test/src/main/groovy/net/wooga/jenkins/pipeline/test/sandbox/PackageWhitelist.groovy similarity index 94% rename from test/groovy/tools/sandbox/PackageWhitelist.groovy rename to atlas-jenkins-pipeline-test/src/main/groovy/net/wooga/jenkins/pipeline/test/sandbox/PackageWhitelist.groovy index 70396835..3931c272 100644 --- a/test/groovy/tools/sandbox/PackageWhitelist.groovy +++ b/atlas-jenkins-pipeline-test/src/main/groovy/net/wooga/jenkins/pipeline/test/sandbox/PackageWhitelist.groovy @@ -1,4 +1,4 @@ -package tools.sandbox +package net.wooga.jenkins.pipeline.test.sandbox import org.jenkinsci.plugins.scriptsecurity.sandbox.Whitelist diff --git a/test/groovy/tools/sandbox/SandboxDeclarativePipelineTest.groovy b/atlas-jenkins-pipeline-test/src/main/groovy/net/wooga/jenkins/pipeline/test/sandbox/SandboxDeclarativePipelineTest.groovy similarity index 85% rename from test/groovy/tools/sandbox/SandboxDeclarativePipelineTest.groovy rename to atlas-jenkins-pipeline-test/src/main/groovy/net/wooga/jenkins/pipeline/test/sandbox/SandboxDeclarativePipelineTest.groovy index b7005084..574d54e8 100644 --- a/test/groovy/tools/sandbox/SandboxDeclarativePipelineTest.groovy +++ b/atlas-jenkins-pipeline-test/src/main/groovy/net/wooga/jenkins/pipeline/test/sandbox/SandboxDeclarativePipelineTest.groovy @@ -1,4 +1,4 @@ -package tools.sandbox +package net.wooga.jenkins.pipeline.test.sandbox import com.lesfurets.jenkins.unit.declarative.DeclarativePipelineTest import org.jenkinsci.plugins.scriptsecurity.sandbox.Whitelist diff --git a/test/groovy/tools/sandbox/SandboxPipelineTestHelper.groovy b/atlas-jenkins-pipeline-test/src/main/groovy/net/wooga/jenkins/pipeline/test/sandbox/SandboxPipelineTestHelper.groovy similarity index 65% rename from test/groovy/tools/sandbox/SandboxPipelineTestHelper.groovy rename to atlas-jenkins-pipeline-test/src/main/groovy/net/wooga/jenkins/pipeline/test/sandbox/SandboxPipelineTestHelper.groovy index c1fc0c56..797bc3c7 100644 --- a/test/groovy/tools/sandbox/SandboxPipelineTestHelper.groovy +++ b/atlas-jenkins-pipeline-test/src/main/groovy/net/wooga/jenkins/pipeline/test/sandbox/SandboxPipelineTestHelper.groovy @@ -1,4 +1,4 @@ -package tools.sandbox +package net.wooga.jenkins.pipeline.test.sandbox import com.lesfurets.jenkins.unit.PipelineTestHelper import org.codehaus.groovy.control.CompilerConfiguration @@ -6,6 +6,7 @@ import org.jenkinsci.plugins.scriptsecurity.sandbox.Whitelist import org.jenkinsci.plugins.scriptsecurity.sandbox.groovy.ClassLoaderWhitelist import org.jenkinsci.plugins.scriptsecurity.sandbox.groovy.GroovySandbox import org.jenkinsci.plugins.scriptsecurity.sandbox.whitelists.ProxyWhitelist +import org.powermock.classloading.DeepCloner /** * Pipeline version test that integrates its own Groovy compilations shenanigans with the groovy sandbox ones. @@ -30,7 +31,7 @@ class SandboxPipelineTestHelper extends PipelineTestHelper { private Whitelist createWhitelist() { return new ProxyWhitelist(whitelist, - new ClassLoaderWhitelist(gse.groovyClassLoader), + new GroovyClassLoaderWhitelist(gse.groovyClassLoader), new MethodSignatureWhitelist(allowedMethodCallbacks.keySet())) } @@ -54,7 +55,8 @@ class SandboxPipelineTestHelper extends PipelineTestHelper { return sandboxGse } - Script loadSandboxedScript(String scriptName, Binding binding) { + @Override + Script loadScript(String scriptName, Binding binding) { Script script = inSandbox { super.loadScript(scriptName, binding) } @@ -70,5 +72,31 @@ class SandboxPipelineTestHelper extends PipelineTestHelper { } } + /** + * Uses powermock's org.powermock.classloading.DeepCloner to deep clone a object to the target classloader. + * Assumes that a identical class is loaded in the target classloader. + * + * @param object - Object to be cloned + * @param cl - Target classloader. Defaults to the classloader used to load this class. + * @return cloned object in the target classloader. + */ + public T cloneTo(T object, ClassLoader cl = this.class.classLoader) { + def deepCloner = new DeepCloner(cl) + return deepCloner.clone(object) + } + + /** + * Deep clones a object to the sandbox classloader. + * Assumes that a identical class is loaded in the sandbox classloader. + * + * @param object - Object to be cloned + * @param cl - Target classloader. Defaults to the classloader used to load this class. + * @return cloned object in the target classloader. + */ + public T cloneToSandbox(T object) { + return cloneTo(object, gse.groovyClassLoader) + } + + } \ No newline at end of file diff --git a/atlas-jenkins-pipeline-test/src/main/groovy/net/wooga/jenkins/pipeline/test/specifications/DeclarativeJenkinsSpec.groovy b/atlas-jenkins-pipeline-test/src/main/groovy/net/wooga/jenkins/pipeline/test/specifications/DeclarativeJenkinsSpec.groovy new file mode 100644 index 00000000..aac7dbf5 --- /dev/null +++ b/atlas-jenkins-pipeline-test/src/main/groovy/net/wooga/jenkins/pipeline/test/specifications/DeclarativeJenkinsSpec.groovy @@ -0,0 +1,174 @@ +package net.wooga.jenkins.pipeline.test.specifications + +import com.lesfurets.jenkins.unit.PipelineTestHelper +import com.lesfurets.jenkins.unit.declarative.DeclarativePipeline +import com.lesfurets.jenkins.unit.declarative.DeclarativePipelineTest +import com.lesfurets.jenkins.unit.declarative.GenericPipelineDeclaration +import com.lesfurets.jenkins.unit.declarative.WhenDeclaration +import net.wooga.jenkins.pipeline.test.specifications.fakes.FakeCredentialStorage +import net.wooga.jenkins.pipeline.test.specifications.fakes.FakeEnvironment +import net.wooga.jenkins.pipeline.test.specifications.fakes.FakeJenkinsObject +import net.wooga.jenkins.pipeline.test.specifications.fakes.FakeMethodCalls +import net.wooga.jenkins.pipeline.test.specifications.fakes.WithCredentials +import net.wooga.jenkins.pipeline.test.exceptions.JenkinsError +import org.apache.commons.lang3.ClassUtils +import spock.lang.Shared +import spock.lang.Specification + +import java.lang.reflect.Method +import java.security.MessageDigest +import java.util.stream.IntStream + +/** + * Abstract test specification for testing Jenkins Declarative Pipeline. + * Has fake environments and credential storage for testing. + */ +abstract class DeclarativeJenkinsSpec extends Specification { + + static Object lock = new Object() + @Shared Map stash + @Shared FakeCredentialStorage credentials + @Shared FakeEnvironment environment + @Shared FakeMethodCalls calls + @Shared PipelineTestHelper helper + @Shared Binding binding + @Shared String currentDir + @Shared GenericPipelineDeclaration pipeline + + abstract DeclarativePipelineTest getJenkinsTest() + + void setupSpec() { + this.stash = new HashMap<>() + this.credentials = new FakeCredentialStorage() + this.environment = new FakeEnvironment(jenkinsTest.binding) + this.calls = new FakeMethodCalls(jenkinsTest.helper) + jenkinsTest.pipelineInterceptor = { Closure cls -> + GenericPipelineDeclaration.binding = binding + this.pipeline = GenericPipelineDeclaration.createComponent(DeclarativePipeline, cls) + this.pipeline.execute(delegate) + } + this.helper = jenkinsTest.helper + this.binding = jenkinsTest.binding + this.currentDir = null + jenkinsTest.setUp() + addLackingDSLTerms() + populateJenkinsDefaultEnvironment(binding) + } + + private static void addLackingDSLTerms() { + GenericPipelineDeclaration.metaClass.any = "any" + WhenDeclaration.metaClass.beforeAgent = { bool -> null } + } + + private static void populateJenkinsDefaultEnvironment(Binding binding) { + binding.with { + setProperty("scm", "scm") + setProperty("BUILD_NUMBER", 1) + setProperty("BRANCH_NAME", "branch") + } + } + + void setup() { + registerPipelineFakeMethods(helper) + } + + void cleanup() { + helper?.callStack?.clear() + environment.wipe() + credentials.wipe() + currentDir = null + } + + /** + * Loads a script to be used by another script. Registers its own call() method as an jenkins mock method, + * in order for it to be used by another script. + * @param path - path to the script to be loaded. + * @param varBindingOps - convenience closure to execute operations over the binding + * object that will be passed to scripts + * @param reloadSideScripts - if the side scripts should be loaded, defaults to true. + * @return Script object representing the loaded script + */ + void registerSideScript(String scriptPath, Binding binding) { + def scriptName = new File(scriptPath).name.replace(".groovy", "") + def script = helper.loadScript(scriptPath, binding) + script.class.getDeclaredMethods().findAll {it.name == "call" }.each { callMethod -> + registerAllowedMethod(scriptName, script, callMethod) + } + } + + /** + * Registers a given Method as a jenkins mock method.. + * @param methodName Name to register the mock as; + * @param base Object from which method will be called + * @param method the method to be called + * @return Closure calling the registered method. + */ + Closure registerAllowedMethod(String methodName, Object base, Method method) { + List methodParams = method.parameterTypes.collect { + return it.isPrimitive()? ClassUtils.primitiveToWrapper(it) : it + } + def methodCall = { Object... args -> + args = args == null? [null] : args + args = IntStream.range(0, args.size()).mapToObj { int index -> + if(args[index] instanceof GString) { + return args[index].toString() + } + return methodParams[index]?.cast(args[index]) + }.toArray() + method.invoke(base, args) + } + helper.registerAllowedMethod(methodName, methodParams, methodCall) + return methodCall + } + + + private void registerPipelineFakeMethods(PipelineTestHelper helper) { + helper.with { + registerAllowedMethod("httpRequest", [LinkedHashMap]) {} + registerAllowedMethod("publishHTML", [HashMap]) {} + registerAllowedMethod("sourceFiles", [String]) {} + registerAllowedMethod("publishCoverage", [Map]) {} + registerAllowedMethod("unstable", [Map]) {} + registerAllowedMethod("error", [String]) { String msg -> throw new JenkinsError(msg) } + registerAllowedMethod("withEnv", [List, Closure], environment.&runWithEnv) + registerAllowedMethod("checkout", [String]) {} + registerAllowedMethod("fileExists", [String]) { String path -> new File(path).exists() } + registerAllowedMethod("readFile", [String]) { String path -> new File(path).text } + registerAllowedMethod("findFiles", [Map]) { Map args -> new FakeJenkinsObject().findFiles(args) } + registerAllowedMethod("usernamePassword", [Map], WithCredentials.&usernamePassword) + registerAllowedMethod("usernameColonPassword", [Map], WithCredentials.&usernameColonPassword) + //this doesn't generate KEY_USR and KEY_PWD, as we don't know who KEY is. This may be possible with groovy AST wizardry though. + registerAllowedMethod("credentials", [String], credentials.&getAt) + registerAllowedMethod("string", [Map]) { Map params -> + return params.containsKey("name")? + jenkinsTest.paramInterceptor : //string() from parameters clause + WithCredentials.string(params) //string() from withCredentials() context + } + registerAllowedMethod("withCredentials", [List.class, Closure.class], + WithCredentials.&bindCredentials.curry(credentials, environment)) + //needed as utils scripts are dependent on jenkins sandbox + registerAllowedMethod("utils", []) { + [stringToSHA1 : { content -> + def shaBytes = MessageDigest.getInstance("SHA1").digest(content.toString().getBytes()) + return new BigInteger(1, shaBytes).toString(16) + }] + } + registerAllowedMethod("lock", [Closure]) { cls -> synchronized (lock) { cls() } } + registerAllowedMethod("stash", [Map]) { params -> stash[params.name] = params } + registerAllowedMethod("unstash", [String]) { String key -> + Optional.ofNullable(stash[key]). + orElseThrow{new IllegalStateException("${key} does not exists on stash")} + } + registerAllowedMethod("dir", [String, Closure]) { String targetDir, cls -> + def previousDir = this.currentDir + try { + this.currentDir = [previousDir, targetDir].findAll {it != null }.join("/") + cls() + } finally { + this.currentDir = previousDir + } + } + } + } + +} diff --git a/atlas-jenkins-pipeline-test/src/main/groovy/net/wooga/jenkins/pipeline/test/specifications/SandboxedDeclarativeJenkinsSpec.groovy b/atlas-jenkins-pipeline-test/src/main/groovy/net/wooga/jenkins/pipeline/test/specifications/SandboxedDeclarativeJenkinsSpec.groovy new file mode 100644 index 00000000..9c1449bc --- /dev/null +++ b/atlas-jenkins-pipeline-test/src/main/groovy/net/wooga/jenkins/pipeline/test/specifications/SandboxedDeclarativeJenkinsSpec.groovy @@ -0,0 +1,84 @@ +package net.wooga.jenkins.pipeline.test.specifications + +import com.lesfurets.jenkins.unit.declarative.DeclarativePipelineTest +import net.wooga.jenkins.pipeline.test.sandbox.PackageWhitelist +import net.wooga.jenkins.pipeline.test.sandbox.SandboxDeclarativePipelineTest +import net.wooga.jenkins.pipeline.test.sandbox.SandboxPipelineTestHelper +import org.jenkinsci.plugins.scriptsecurity.sandbox.Whitelist +import org.jenkinsci.plugins.scriptsecurity.sandbox.whitelists.GenericWhitelist +import org.jenkinsci.plugins.scriptsecurity.sandbox.whitelists.ProxyWhitelist + +abstract class SandboxedDeclarativeJenkinsSpec extends DeclarativeJenkinsSpec { + + + abstract String[] getWhitelistedPackages() + DeclarativePipelineTest jenkinsTest; + + @Override + DeclarativePipelineTest getJenkinsTest() { + if(jenkinsTest == null) { + def extraWhitelist = whitelistedPackages.collect { new PackageWhitelist(it) } + def whitelist = [new GenericWhitelist(), + new PackageWhitelist("com.lesfurets.jenkins")] + whitelist.addAll(extraWhitelist) + jenkinsTest = new SandboxDeclarativePipelineTest(new ProxyWhitelist(whitelist)) + } + return jenkinsTest + } + + @Override + SandboxPipelineTestHelper getHelper() { + return super.helper as SandboxPipelineTestHelper + } + + /** + * Runs a closure in sandbox environment. + *

+ * IMPORTANT:
+ * Object exchange from in to outside the sandbox environment (ie. parameters pass or returns), is non-trivial. + * As the sandbox environment has its own ClassLoader, trying to use any 'non-basic' + * (ie. not loaded by the bootstrap ClassLoader) JVM object will fail. + * If you are having trouble with inane ClassNotFoundException(s), this is probably the case. + *

+ * Example of safe classes are Map, String, Object, and primitives, but there are many others besides these. + *

+ * If you have to pass in a custom object, `helper.cloneToSandbox` will generate a clone of that object in the sandbox classLoader, + * which then can be used inside the sandbox. + * For returns, you can use inSandboxClonedReturn or `helper.cloneTo`, to generate a clone of a sandboxed object in a non-sandbox environment. + *
+ * @param cls function to run into the sandbox + * @return the direct return of the cls closure + */ + protected T inSandbox(Closure cls) { + return helper.inSandbox(cls) + } + + /** + * Runs a closure in sandbox environment. + * Limitations from inSandbox(Closure) still applies, except for the return object, + * which is transfered to this class classLoader. + * + * @param cls function to run into the sandbox + * @return cloned return of the cls closure, transplanted to this class' class loader. + */ + protected T inSandboxClonedReturn(Closure cls) { + def sandboxedReturn = inSandbox { cls() } + return helper.cloneTo(sandboxedReturn, this.class.classLoader) + } + + /** + * Loads a script and the javaLibCheck and publish side scripts inside the sandbox. + * @param path - path to the script to be loaded. + * @param sideScripts - helper scripts to that will be loaded beforehand and in the same binding as your desired script + * object that will be passed to scripts + * @param varBindingOps - convenience closure to execute operations over the binding + * object that will be passed to scripts + * @return Script object representing the loaded script + */ + Script loadSandboxedScript(String path, List sideScripts=[], Closure varBindingOps={}) { + varBindingOps.setDelegate(binding.variables) + varBindingOps(binding.variables) + sideScripts.each { registerSideScript(it, binding) } + return helper.loadScript(path, binding) + } +} diff --git a/test/groovy/tools/FakeCredentialStorage.groovy b/atlas-jenkins-pipeline-test/src/main/groovy/net/wooga/jenkins/pipeline/test/specifications/fakes/FakeCredentialStorage.groovy similarity index 89% rename from test/groovy/tools/FakeCredentialStorage.groovy rename to atlas-jenkins-pipeline-test/src/main/groovy/net/wooga/jenkins/pipeline/test/specifications/fakes/FakeCredentialStorage.groovy index c4fbe884..f5eb3c38 100644 --- a/test/groovy/tools/FakeCredentialStorage.groovy +++ b/atlas-jenkins-pipeline-test/src/main/groovy/net/wooga/jenkins/pipeline/test/specifications/fakes/FakeCredentialStorage.groovy @@ -1,4 +1,4 @@ -package tools +package net.wooga.jenkins.pipeline.test.specifications.fakes class FakeCredentialStorage { diff --git a/test/groovy/tools/FakeEnvironment.groovy b/atlas-jenkins-pipeline-test/src/main/groovy/net/wooga/jenkins/pipeline/test/specifications/fakes/FakeEnvironment.groovy similarity index 63% rename from test/groovy/tools/FakeEnvironment.groovy rename to atlas-jenkins-pipeline-test/src/main/groovy/net/wooga/jenkins/pipeline/test/specifications/fakes/FakeEnvironment.groovy index 06db7b3f..cc78e5c6 100644 --- a/test/groovy/tools/FakeEnvironment.groovy +++ b/atlas-jenkins-pipeline-test/src/main/groovy/net/wooga/jenkins/pipeline/test/specifications/fakes/FakeEnvironment.groovy @@ -1,11 +1,11 @@ -package tools +package net.wooga.jenkins.pipeline.test.specifications.fakes class FakeEnvironment { - final List usedEnvironments + final List used final Binding binding FakeEnvironment(Binding binding) { - this.usedEnvironments = new ArrayList<>() + this.used = new ArrayList<>() this.binding = binding } @@ -18,13 +18,19 @@ class FakeEnvironment { binding.variables[key] = value } - def runWithEnv(Map env, Closure cls) { + void runWithEnv(List envStrs, Closure cls) { + def envMap = envStrs. + collect{it.toString()}. + collectEntries{String envStr -> [(envStr.split("=")[0]): envStr.split("=")[1]]} + runWithEnv(envMap, cls) + } + void runWithEnv(Map env, Closure cls) { binding.env.putAll(env) binding.variables.putAll(env) try { cls() } finally { - usedEnvironments.add(deepCopy(binding.env as Map)) + used.add(deepCopy(binding.env as Map)) env.each { binding.env.remove(it.key) binding.variables.remove(it.key) @@ -42,6 +48,6 @@ class FakeEnvironment { } void wipe() { - usedEnvironments.clear() + used.clear() } } diff --git a/test/groovy/tools/FakeJenkinsObject.groovy b/atlas-jenkins-pipeline-test/src/main/groovy/net/wooga/jenkins/pipeline/test/specifications/fakes/FakeJenkinsObject.groovy similarity index 67% rename from test/groovy/tools/FakeJenkinsObject.groovy rename to atlas-jenkins-pipeline-test/src/main/groovy/net/wooga/jenkins/pipeline/test/specifications/fakes/FakeJenkinsObject.groovy index 66546f69..a502874c 100644 --- a/test/groovy/tools/FakeJenkinsObject.groovy +++ b/atlas-jenkins-pipeline-test/src/main/groovy/net/wooga/jenkins/pipeline/test/specifications/fakes/FakeJenkinsObject.groovy @@ -1,4 +1,4 @@ -package tools +package net.wooga.jenkins.pipeline.test.specifications.fakes import java.nio.file.FileSystems import java.nio.file.Files @@ -13,10 +13,10 @@ class FakeJenkinsObject extends LinkedHashMap { final PathMatcher matcher = FileSystems.getDefault().getPathMatcher("glob:${glob}"); def stream = Files.walk(Paths.get(".")); try { - return stream. - filter {matcher.matches(it) }. - map { it.toFile().absolutePath }. - collect(Collectors.toList()) + return stream. + filter {matcher.matches(it) }. + map { it.toFile().absolutePath }. + collect(Collectors.toList()) } finally { stream.close() } diff --git a/test/groovy/tools/FakeMethodCalls.groovy b/atlas-jenkins-pipeline-test/src/main/groovy/net/wooga/jenkins/pipeline/test/specifications/fakes/FakeMethodCalls.groovy similarity index 90% rename from test/groovy/tools/FakeMethodCalls.groovy rename to atlas-jenkins-pipeline-test/src/main/groovy/net/wooga/jenkins/pipeline/test/specifications/fakes/FakeMethodCalls.groovy index 377d615c..9cefa788 100644 --- a/test/groovy/tools/FakeMethodCalls.groovy +++ b/atlas-jenkins-pipeline-test/src/main/groovy/net/wooga/jenkins/pipeline/test/specifications/fakes/FakeMethodCalls.groovy @@ -1,4 +1,4 @@ -package tools +package net.wooga.jenkins.pipeline.test.specifications.fakes import com.lesfurets.jenkins.unit.MethodCall import com.lesfurets.jenkins.unit.PipelineTestHelper diff --git a/test/groovy/tools/WithCredentials.groovy b/atlas-jenkins-pipeline-test/src/main/groovy/net/wooga/jenkins/pipeline/test/specifications/fakes/WithCredentials.groovy similarity index 97% rename from test/groovy/tools/WithCredentials.groovy rename to atlas-jenkins-pipeline-test/src/main/groovy/net/wooga/jenkins/pipeline/test/specifications/fakes/WithCredentials.groovy index e955a828..3d57099b 100644 --- a/test/groovy/tools/WithCredentials.groovy +++ b/atlas-jenkins-pipeline-test/src/main/groovy/net/wooga/jenkins/pipeline/test/specifications/fakes/WithCredentials.groovy @@ -1,4 +1,4 @@ -package tools +package net.wooga.jenkins.pipeline.test.specifications.fakes class WithCredentials { diff --git a/build.gradle b/build.gradle index da64e76c..aba0639f 100644 --- a/build.gradle +++ b/build.gradle @@ -29,17 +29,8 @@ dependencies { implementation 'com.cloudbees:groovy-cps:1.12' implementation group: 'org.jenkinsci.plugins', name: 'pipeline-model-definition', version: '1.2', ext: 'jar' implementation 'org.codehaus.groovy:groovy-all:2.4.21' - - testImplementation "com.lesfurets:jenkins-pipeline-unit:1.9" testImplementation "org.spockframework:spock-core:1.3-groovy-2.4" - testImplementation group: "org.jenkins-ci.plugins", name: "script-security", version:"1158.v7c1b_73a_69a_08", ext: "jar" - testImplementation group: "org.kohsuke", name: "groovy-sandbox", version:"1.27", ext: "jar" - - //needed jenkins stuff used implicitly - testImplementation("org.jenkins-ci.main:jenkins-core:2.332.2") { - exclude module: "groovy-all" - } - testImplementation "org.jenkins-ci.main:jenkins-test-harness:2.71" + testImplementation project(':atlas-jenkins-pipeline-test') testImplementation 'com.github.ben-manes.caffeine:caffeine:2.9.2' } diff --git a/settings.gradle b/settings.gradle index 9b68c255..d3294a7f 100644 --- a/settings.gradle +++ b/settings.gradle @@ -4,3 +4,6 @@ rootProject.name = 'atlas-jenkins-pipeline' includeBuild('atlas-jenkins-pipeline-gradle') + +include 'atlas-jenkins-pipeline-test' +project(':atlas-jenkins-pipeline-test').projectDir = file('atlas-jenkins-pipeline-test') diff --git a/test/groovy/net/wooga/jenkins/pipeline/config/WDKConfigSpec.groovy b/test/groovy/net/wooga/jenkins/pipeline/config/WDKConfigSpec.groovy index 7e05a590..50f72914 100644 --- a/test/groovy/net/wooga/jenkins/pipeline/config/WDKConfigSpec.groovy +++ b/test/groovy/net/wooga/jenkins/pipeline/config/WDKConfigSpec.groovy @@ -1,15 +1,15 @@ package net.wooga.jenkins.pipeline.config import net.wooga.jenkins.pipeline.BuildVersion +import net.wooga.jenkins.pipeline.test.specifications.fakes.FakeJenkinsObject import spock.lang.Shared import spock.lang.Specification import spock.lang.Unroll -import tools.FakeJenkinsObject class WDKConfigSpec extends Specification { @Shared - def jenkinsScript = new FakeJenkinsObject([BUILD_NUMBER: 1, BRANCH_NAME: "branch"]) + def jenkinsScript = [BUILD_NUMBER: 1, BRANCH_NAME: "branch"] @Unroll("creates valid WDKConfig object from #configMap") diff --git a/test/groovy/scripts/BuildJavaLibrarySpec.groovy b/test/groovy/scripts/BuildJavaLibrarySpec.groovy index e68d20c3..1748e17e 100644 --- a/test/groovy/scripts/BuildJavaLibrarySpec.groovy +++ b/test/groovy/scripts/BuildJavaLibrarySpec.groovy @@ -16,7 +16,7 @@ class BuildJavaLibrarySpec extends DeclarativeJenkinsSpec { given: "loaded buildJavaLibrary in a successful build" helper.registerAllowedMethod("httpRequest", [LinkedHashMap]) {} def buildJavaLibrary = loadSandboxedScript(SCRIPT_PATH) { - currentBuild["result"] = "SUCCESS" + currentBuild["result"] = "SUCCESS" } and: "a coveralls token" diff --git a/test/groovy/scripts/BuildWDKJSSpec.groovy b/test/groovy/scripts/BuildWDKJSSpec.groovy index 8ed4e7a6..29be1ba6 100644 --- a/test/groovy/scripts/BuildWDKJSSpec.groovy +++ b/test/groovy/scripts/BuildWDKJSSpec.groovy @@ -244,7 +244,9 @@ class BuildWDKJSSpec extends DeclarativeJenkinsSpec { def buildWDKJS = loadSandboxedScript(SCRIPT_PATH) and: "configuration object with any platforms and wrappers for test result capture" def stepsDirs = [] + def checkoutDir = "." def configMap = [platforms: ["linux"], checkDir: checkDir, + checkoutDir: checkoutDir, testWrapper: { testOp, platform -> stepsDirs.add(this.currentDir) testOp(platform) @@ -263,7 +265,7 @@ class BuildWDKJSSpec extends DeclarativeJenkinsSpec { then: "steps ran on given directory" //checks steps + 1 analysis step stepsDirs.size() == configMap.platforms.size() + 1 - stepsDirs.every {it == checkDir} + stepsDirs.every {it == "${checkoutDir}/${checkDir}"} where: checkDir << [".", "dir", "dir/subdir"] diff --git a/test/groovy/scripts/JavaCheckSpec.groovy b/test/groovy/scripts/JavaCheckSpec.groovy index 010c22a2..40c29a97 100644 --- a/test/groovy/scripts/JavaCheckSpec.groovy +++ b/test/groovy/scripts/JavaCheckSpec.groovy @@ -352,7 +352,9 @@ class JavaCheckSpec extends DeclarativeJenkinsSpec { def check = loadSandboxedScript(TEST_SCRIPT_PATH) and: "configuration object with any platforms and wrappers for test result capture" def stepsDirs = [] + def checkoutDir = "." def configMap = [platforms: ["linux"], checkDir: checkDir, + checkoutDir: checkoutDir, testWrapper: { testOp, platform -> stepsDirs.add(this.currentDir) testOp(platform) @@ -371,7 +373,7 @@ class JavaCheckSpec extends DeclarativeJenkinsSpec { then: "steps ran on given directory" //checks steps + 1 analysis step stepsDirs.size() == configMap.platforms.size() + 1 - stepsDirs.every {it == checkDir} + stepsDirs.every {it == "${checkoutDir}/${checkDir}"} where: checkDir << [".", "dir", "dir/subdir"] diff --git a/test/groovy/scripts/WDKCheckSpec.groovy b/test/groovy/scripts/WDKCheckSpec.groovy index f12504e2..3a87258b 100644 --- a/test/groovy/scripts/WDKCheckSpec.groovy +++ b/test/groovy/scripts/WDKCheckSpec.groovy @@ -25,7 +25,7 @@ class WDKCheckSpec extends DeclarativeJenkinsSpec { helper.registerAllowedMethod("sourceFiles", [String]) { it -> it } helper.registerAllowedMethod("publishCoverage", [Map]) { it -> it } and: "stashed setup data" - jenkinsStash["setup_w"] = [useDefaultExcludes: true, includes: "paket.lock .gradle/**, **/build/**, .paket/**, packages/**, paket-files/**, **/Paket.Unity3D/**, **/Wooga/Plugins/**"] + stash["setup_w"] = [useDefaultExcludes: true, includes: "paket.lock .gradle/**, **/build/**, .paket/**, packages/**, paket-files/**, **/Paket.Unity3D/**, **/Wooga/Plugins/**"] when: "running gradle pipeline with coverage token" inSandbox { @@ -55,7 +55,7 @@ class WDKCheckSpec extends DeclarativeJenkinsSpec { def configMap = [unityVersions: "2019", sonarToken: sonarToken] and: "stashed setup data" - jenkinsStash[PipelineConventions.standard.wdkSetupStashId] = [:] + stash[PipelineConventions.standard.wdkSetupStashId] = [:] when: "running check with sonarqube token" inSandbox { @@ -86,7 +86,7 @@ class WDKCheckSpec extends DeclarativeJenkinsSpec { def config = [unityVersions: "2019"] and: "stashed setup data" - jenkinsStash[PipelineConventions.standard.wdkSetupStashId] = [:] + stash[PipelineConventions.standard.wdkSetupStashId] = [:] when: "running check without sonarqube token" inSandbox { @@ -116,7 +116,7 @@ class WDKCheckSpec extends DeclarativeJenkinsSpec { def configMap = [unityVersions: "2019", sonarToken: "sonarToken"] and: "stashed setup data" - jenkinsStash[PipelineConventions.standard.wdkSetupStashId] = [:] + stash[PipelineConventions.standard.wdkSetupStashId] = [:] when: "running check with sonarqube token" inSandbox { @@ -148,7 +148,7 @@ class WDKCheckSpec extends DeclarativeJenkinsSpec { and: "configuration object with given platforms" def configMap = [unityVersions: versions, wdkSetupStashId: setupStash] and: "stashed setup data" - jenkinsStash[setupStash] = [:] + stash[setupStash] = [:] and: "wired checkout call" def checkoutDirs = [] helper.registerAllowedMethod("checkout", [String]) { @@ -346,7 +346,7 @@ class WDKCheckSpec extends DeclarativeJenkinsSpec { wdkSetupStashId : convWDKSetupStashId ] and: "stashed setup data" - jenkinsStash[convWDKSetupStashId] = [:] + stash[convWDKSetupStashId] = [:] when: "running check" def checkSteps = inSandbox { @@ -392,7 +392,7 @@ class WDKCheckSpec extends DeclarativeJenkinsSpec { analysisOp(unityPlatform) }] and: "stashed setup data" - jenkinsStash[PipelineConventions.standard.wdkSetupStashId] = [:] + stash[PipelineConventions.standard.wdkSetupStashId] = [:] when: "running check" inSandbox { @@ -414,7 +414,7 @@ class WDKCheckSpec extends DeclarativeJenkinsSpec { and: "configuration object with any platforms" def configMap = [unityVersions: ["2019"], checkoutDir: checkoutDir] and: "stashed setup" - jenkinsStash[PipelineConventions.standard.wdkSetupStashId] = [:] + stash[PipelineConventions.standard.wdkSetupStashId] = [:] when: "running check" def actualCheckoutDir = "" @@ -440,7 +440,9 @@ class WDKCheckSpec extends DeclarativeJenkinsSpec { def check = loadSandboxedScript(TEST_SCRIPT_PATH) and: "configuration object with any platforms and wrappers for test assertion" def stepsDirs = [] + def checkoutDir = "." def configMap = [unityVersions: ["2019"], checkDir: checkDir, + checkoutDir: checkoutDir, testWrapper: { testOp, platform -> stepsDirs.add(this.currentDir) testOp(platform) @@ -450,7 +452,7 @@ class WDKCheckSpec extends DeclarativeJenkinsSpec { analysisOp(platform) }] and: "stashed setup" - jenkinsStash["setup_w"] = [:] + stash["setup_w"] = [:] when: "running check" @@ -462,7 +464,7 @@ class WDKCheckSpec extends DeclarativeJenkinsSpec { then: "steps ran on given directory" //checks steps + 1 analysis step stepsDirs.size() == configMap.unityVersions.size() + 1 - stepsDirs.every { it == checkDir } + stepsDirs.every {it == "${checkoutDir}/${checkDir}"} where: checkDir << [".", "dir", "dir/subdir"] @@ -475,7 +477,7 @@ class WDKCheckSpec extends DeclarativeJenkinsSpec { and: "a configuration with a mandatory platform and clearWs" def configMap = [unityVersions: versions, clearWs: clearWs] and: "stashed setup" - jenkinsStash[PipelineConventions.standard.wdkSetupStashId] = [:] + stash[PipelineConventions.standard.wdkSetupStashId] = [:] when: "running check" inSandbox { diff --git a/test/groovy/tools/DeclarativeJenkinsSpec.groovy b/test/groovy/tools/DeclarativeJenkinsSpec.groovy index cada2c6b..7e9f5386 100644 --- a/test/groovy/tools/DeclarativeJenkinsSpec.groovy +++ b/test/groovy/tools/DeclarativeJenkinsSpec.groovy @@ -1,230 +1,27 @@ package tools +import net.wooga.jenkins.pipeline.test.specifications.SandboxedDeclarativeJenkinsSpec -import com.lesfurets.jenkins.unit.PipelineTestHelper -import com.lesfurets.jenkins.unit.declarative.DeclarativePipeline -import com.lesfurets.jenkins.unit.declarative.DeclarativePipelineTest -import com.lesfurets.jenkins.unit.declarative.GenericPipelineDeclaration -import com.lesfurets.jenkins.unit.declarative.PostDeclaration -import com.lesfurets.jenkins.unit.declarative.WhenDeclaration -import org.apache.commons.lang3.ClassUtils -import org.jenkinsci.plugins.scriptsecurity.sandbox.whitelists.GenericWhitelist -import org.jenkinsci.plugins.scriptsecurity.sandbox.whitelists.ProxyWhitelist -import spock.lang.Shared -import spock.lang.Specification -import tools.sandbox.PackageWhitelist -import tools.sandbox.SandboxDeclarativePipelineTest -import tools.sandbox.SandboxPipelineTestHelper - -import java.lang.reflect.Method -import java.util.stream.IntStream - -/** - * Abstract test specification for testing Jenkins Declarative Pipeline. - * Has fake environments and credential storage for testing, as well as support for sandboxed execution. - */ -abstract class DeclarativeJenkinsSpec extends Specification { - - static Object lock = new Object() - @Shared DeclarativePipelineTest jenkinsTest; - @Shared Binding binding - @Shared SandboxPipelineTestHelper helper - @Shared FakeCredentialStorage credentials - @Shared FakeMethodCalls calls - @Shared FakeEnvironment environment - @Shared Map jenkinsStash - @Shared String currentDir - @Shared GenericPipelineDeclaration pipeline +class DeclarativeJenkinsSpec extends SandboxedDeclarativeJenkinsSpec { def setupSpec() { - jenkinsStash = new HashMap<>() - jenkinsTest = new SandboxDeclarativePipelineTest(new ProxyWhitelist( - new GenericWhitelist(), - new PackageWhitelist("com.lesfurets.jenkins"), - new PackageWhitelist("net.wooga.jenkins.pipeline") - )) - jenkinsTest.pipelineInterceptor = { Closure cls -> - GenericPipelineDeclaration.binding = binding - this.pipeline = GenericPipelineDeclaration.createComponent(DeclarativePipeline, cls) - this.pipeline.execute(delegate) - } - jenkinsTest.setUp() - credentials = new FakeCredentialStorage() - environment = new FakeEnvironment(jenkinsTest.binding) - calls = new FakeMethodCalls(jenkinsTest.helper) - - helper = jenkinsTest.helper as SandboxPipelineTestHelper - binding = jenkinsTest.binding - currentDir = null - - addLackingDSLTerms() - populateJenkinsDefaultEnvironment(binding) - } - - def setup() { - registerPipelineFakeMethods(helper) - } - - def cleanup() { - helper?.callStack?.clear() - environment.wipe() - credentials.wipe() - } - - def getUsedEnvironments() { - return environment.usedEnvironments - } - - private static void populateJenkinsDefaultEnvironment(Binding binding) { binding.with { - setProperty("scm", "scm") - setProperty("BUILD_NUMBER", 1) - setProperty("BRANCH_NAME", "any") setProperty("docker", [:]) - } } - private void registerPipelineFakeMethods(PipelineTestHelper helper) { + def setup() { helper.with { - registerAllowedMethod("httpRequest", [LinkedHashMap]) {} + registerAllowedMethod("istanbulCoberturaAdapter", [String]) {} registerAllowedMethod("isUnix") { true } registerAllowedMethod("sendSlackNotification", [String, boolean]) {} registerAllowedMethod("junit", [LinkedHashMap]) {} registerAllowedMethod("nunit", [LinkedHashMap]) {} - registerAllowedMethod("istanbulCoberturaAdapter", [String]) {} - registerAllowedMethod("sourceFiles", [String]) {} - registerAllowedMethod("publishCoverage", [Map]) {} - registerAllowedMethod("unstash", [String]) {} - registerAllowedMethod("unstable", [Map]) {} - registerAllowedMethod("error", [String]) { String msg -> throw new Exception(msg) } - registerAllowedMethod("withEnv", [List, Closure]) { List envStrs, Closure cls -> - def env = envStrs.collect{it.toString()}. - collectEntries{String envStr -> [(envStr.split("=")[0]): envStr.split("=")[1]]} - environment.runWithEnv(env, cls) - } - registerAllowedMethod("checkout", [String]) {} - registerAllowedMethod("publishHTML", [HashMap]) {} - registerAllowedMethod("fileExists", [String]) { String path -> new File(path).exists() } - registerAllowedMethod("readFile", [String]) { String path -> new File(path).text } - registerAllowedMethod("findFiles", [Map]) { Map args -> new FakeJenkinsObject().findFiles(args) } - registerAllowedMethod("usernamePassword", [Map], WithCredentials.&usernamePassword) - registerAllowedMethod("usernameColonPassword", [Map], WithCredentials.&usernameColonPassword) - //TODO: make this generate KEY_USR and KEY_PWD environment - registerAllowedMethod("credentials", [String], credentials.&getAt) - registerAllowedMethod("string", [Map]) { Map params -> - return params.containsKey("name")? - jenkinsTest.paramInterceptor : //string() from parameters clause - WithCredentials.string(params) //string() from withCredentials() context - } - registerAllowedMethod("withCredentials", [List.class, Closure.class], - WithCredentials.&bindCredentials.curry(credentials, environment)) - //needed as utils scripts are dependent on jenkins sandbox - registerAllowedMethod("utils", []) {[stringToSHA1 : { content -> "fakesha" }]} - registerAllowedMethod("lock", [Closure]) { cls -> - synchronized (lock) { cls() } - } - registerAllowedMethod("stash", [Map]) {Map params -> jenkinsStash[params.name] = params} - registerAllowedMethod("unstash", [String]) { String key -> - Optional.ofNullable(jenkinsStash[key]). - orElseThrow{new IllegalStateException("${key} does not exists on stash")} - } - registerAllowedMethod("dir", [String, Closure]) { String targetDir, cls -> - def previousDir = this.currentDir - this.currentDir = targetDir - cls() - this.currentDir = previousDir - } } } - private static void addLackingDSLTerms() { - GenericPipelineDeclaration.metaClass.any = "any" - WhenDeclaration.metaClass.beforeAgent = { bool -> null } -// PostDeclaration.metaClass.cleanup = { cls -> cls() } -// PostDeclaration.metaClass.script = { cls -> cls() } -// PostDeclaration.metaClass.cleanWs = {} - } - - /** - * Loads a script and the javaLibCheck and publish side scripts inside the sandbox. - * @param path - path to the script to be loaded. - * @param varBindingOps - convenience closure to execute operations over the binding - * object that will be passed to scripts - * @param reloadSideScripts - if the side scripts should be loaded, defaults to true. - * @return Script object representing the loaded script - */ - Script loadSandboxedScript(String path, Closure varBindingOps={}, boolean reloadSideScripts = true) { - varBindingOps.setDelegate(binding.variables) - varBindingOps(binding.variables) - if(reloadSideScripts) { - registerSideScript("vars/javaLibs.groovy", binding) - } - return helper.loadSandboxedScript(path, binding) - } - - - /** - * Loads a script to be used by another script. Registers its own call() method as an jenkins mock method, - * in order for it to be used by another script. - * @param path - path to the script to be loaded. - * @param varBindingOps - convenience closure to execute operations over the binding - * object that will be passed to scripts - * @param reloadSideScripts - if the side scripts should be loaded, defaults to true. - * @return Script object representing the loaded script - */ - protected void registerSideScript(String scriptPath, Binding binding) { - def scriptName = new File(scriptPath).name.replace(".groovy", "") - def script = helper.loadSandboxedScript(scriptPath, binding) - script.class.getDeclaredMethods().findAll {it.name == "call" }.each { callMethod -> - registerAllowedMethod(scriptName, script, callMethod) - } - } - - /** - * Registers a given Method as a jenkins mock method.. - * @param methodName Name to register the mock as; - * @param base Object from which method will be called - * @param method the method to be called - * @return Closure calling the registered method. - */ - protected Closure registerAllowedMethod(String methodName, Object base, Method method) { - List methodParams = method.parameterTypes.collect { - return it.isPrimitive()? ClassUtils.primitiveToWrapper(it) : it - } - def methodCall = { Object... args -> - args = args == null? [null] : args - args = IntStream.range(0, args.size()).mapToObj { int index -> - if(args[index] instanceof GString) { - return args[index].toString() - } - return methodParams[index]?.cast(args[index]) - }.toArray() - method.invoke(base, args) - } - helper.registerAllowedMethod(methodName, methodParams, methodCall) - return methodCall - } - - /** - * Runs a closure in sandbox environment. - * - * IMPORTANT: - * Avoid object exchange from in to outside the sandbox environment (ie. parameters pass or returns). - * As the sandbox environment has its own ClassLoader, trying to use any 'non-basic' - * (ie. not loaded by the bootstrap ClassLoader) JVM object will fail. - * If you are having trouble with inane ClassNotFoundException(s), this is probably the case. - * - * Example of safe classes are Map, String, Object, and primitives, but there are many others besides these. - * - * If you absolutely have to pass a custom object, you can try serializing the it, passing the bytes, - * and then deserialize it on the other side, for instance, using ObjectOutputStream/ObjectInputStream. - * - * @param cls function to run into the sandbox - * @return the return of the cls closure - */ - protected T inSandbox(Closure cls) { - return helper.inSandbox(cls) + List getUsedEnvironments() { + return environment.used } String[] getShGradleCalls() { @@ -233,5 +30,12 @@ abstract class DeclarativeJenkinsSpec extends Specification { } } + @Override + String[] getWhitelistedPackages() { + return ["net.wooga.jenkins.pipeline"] + } + Script loadSandboxedScript(String path, Closure varBindingOps={}) { + return super.loadSandboxedScript(path, ["vars/javaLibs.groovy"], varBindingOps) + } }