Skip to content

Commit

Permalink
Merge pull request jenkinsci#8 from lesfurets/feature_global_lib
Browse files Browse the repository at this point in the history
Test scripts with Pipeline Shared Libraries
  • Loading branch information
EQuincerot authored Mar 14, 2017
2 parents e12b412 + 540ab88 commit fb627ee
Show file tree
Hide file tree
Showing 50 changed files with 1,201 additions and 134 deletions.
85 changes: 85 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,91 @@ This will work fine for such a project structure:
└── TestExampleJob.groovy
```

## Testing Shared Libraries

With [Shared Libraries](https://jenkins.io/doc/book/pipeline/shared-libraries/) Jenkins lets you share common code
on pipelines across different repositories of your organization.
Shared libraries are configured via a settings interface in Jenkins and imported
with `@Library` annotation in your scripts.

Testing pipeline scripts using external libraries is not trivial because the shared library code
is checked in another repository.
JenkinsPipelineUnit lets you test shared libraries and pipelines depending on these libraries.

Here is an example pipeline using a shared library:

```groovy
@Library('commons')
import net.courtanet.jenkins.Utils
sayHello 'World'
node() {
stage ('Checkout') {
def utils = new Utils()
checkout "${utils.gitTools()}"
}
stage ('Build') {
sh './gradlew build'
}
stage ('Post Build') {
String json = libraryResource 'net/courtanet/jenkins/request.json'
sh "curl -H 'Content-Type: application/json' -X POST -d '$json' ${acme.url}"
}
}
```

This pipeline is using a shared library called `commons`.
Now lets test it:

```groovy
String clonePath = 'path/to/clone'
def library = library()
.name('commons')
.retriever(gitSource('git@gitlab.admin.courtanet.net:devteam/lesfurets-jenkins-shared.git'))
.targetPath(clonePath)
.defaultVersion("master")
.allowOverride(true)
.implicit(false)
.build()
helper.registerSharedLibrary(library)
loadScript("job/library/exampleJob.jenkins")
printCallStack()
```

Notice how we defined the shared library and registered it to the helper.
Library definition is done via a fluent API which lets you set the same configurations as in
[Jenkins Global Pipeline Libraries](https://jenkins.io/doc/book/pipeline/shared-libraries/#using-libraries).

The `retriever` and `targetPath` fields tell the framework how to fetch the sources of the library, in which local path.
The framework comes with two naive but useful retrievers, `gitSource` and `localSource`.
You can write your own retriever by implementing the `SourceRetriever` interface.

Note that properties `defaultVersion`, `allowOverride` and `implicit` are optional with
default values `master`, `true` and `false`.

Now if we execute this test, the framework will fetch the sources from the Git repository and
load classes, scripts, global variables and resources found in the library.
The callstack of this execution will look like the following:

```text
Loading shared library commons with version master
libraryJob.run()
libraryJob.sayHello(World)
sayHello.echo(Hello, World.)
libraryJob.node(groovy.lang.Closure)
libraryJob.stage(Checkout, groovy.lang.Closure)
Utils.gitTools()
libraryJob.checkout({branch=master})
libraryJob.stage(Build, groovy.lang.Closure)
libraryJob.sh(./gradlew build)
libraryJob.stage(Post Build, groovy.lang.Closure)
libraryJob.libraryResource(net/courtanet/jenkins/request.json)
libraryJob.sh(curl -H 'Content-Type: application/json' -X POST -d '{"name" : "Ben"}' http://acme.com)
```

## Note on CPS

If you already fiddled with Jenkins pipeline DSL, you experienced strange errors during execution on Jenkins.
Expand Down
17 changes: 6 additions & 11 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,12 @@ archivesBaseName = "jenkins-pipeline-unit"
version = "0.12-SNAPSHOT"

dependencies {
compile group: 'org.codehaus.groovy', name: 'groovy-all', version: '2.4.6'
compile group: 'com.cloudbees', name: 'groovy-cps', version: '1.11'
compile group: 'org.assertj', name: 'assertj-core', version: '3.4.1'
compile group: 'org.codehaus.groovy', name: 'groovy-all', version: '2.4.6'
compile group: 'com.cloudbees', name: 'groovy-cps', version: '1.12'
compile group: 'commons-io', name: 'commons-io', version: '2.5'
compile group: 'org.apache.ivy', name: 'ivy', version: '2.4.0'
compileOnly group: 'org.assertj', name: 'assertj-core', version: '3.4.1'
testCompile group: 'org.assertj', name: 'assertj-core', version: '3.4.1'
testCompile group: 'junit', name: 'junit', version: '4.11'
}

Expand All @@ -37,14 +40,6 @@ if (project.hasProperty('extProps')) {
}
}

sourceSets {
main {
resources {
srcDirs = ['src/main/jenkins']
include '**/*.jenkins'
}
}
}

task javadocJar(type: Jar) {
classifier = 'javadoc'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ abstract class BasePipelineTest {

Binding binding = new Binding()

ClassLoader baseClassLoader = this.class.classLoader

def stringInterceptor = { m -> m.variable }

def withCredentialsInterceptor = { list, closure ->
Expand All @@ -31,15 +33,16 @@ abstract class BasePipelineTest {

BasePipelineTest() {
helper = new PipelineTestHelper()
helper.setScriptRoots scriptRoots
helper.setScriptExtension scriptExtension
helper.setBaseClassloader this.class.classLoader
helper.setImports imports
helper.setBaseScriptRoot baseScriptRoot
}

void setUp() throws Exception {
helper.setScriptRoots scriptRoots
helper.setScriptExtension scriptExtension
helper.setBaseClassloader baseClassLoader
helper.imports += imports
helper.setBaseScriptRoot baseScriptRoot
helper.build()

helper.registerAllowedMethod("stage", [String.class, Closure.class], null)
helper.registerAllowedMethod("stage", [String.class, Closure.class], null)
helper.registerAllowedMethod("node", [String.class, Closure.class], null)
Expand Down Expand Up @@ -75,7 +78,7 @@ abstract class BasePipelineTest {
* Can be useful when mocking a jenkins method.
* @param status job status to set
*/
void updateBuildStatus(String status){
void updateBuildStatus(String status) {
binding.getVariable('currentBuild').result = status
}

Expand Down
26 changes: 26 additions & 0 deletions src/main/groovy/com/lesfurets/jenkins/unit/InterceptingGCL.groovy
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package com.lesfurets.jenkins.unit

import org.codehaus.groovy.control.CompilationFailedException
import org.codehaus.groovy.control.CompilerConfiguration

class InterceptingGCL extends GroovyClassLoader {

PipelineTestHelper helper

InterceptingGCL(PipelineTestHelper helper,
ClassLoader loader,
CompilerConfiguration config) {
super(loader, config)
this.helper = helper
}

@Override
Class parseClass(GroovyCodeSource codeSource, boolean shouldCacheSource)
throws CompilationFailedException {
Class clazz = super.parseClass(codeSource, shouldCacheSource)
clazz.metaClass.invokeMethod = helper.getMethodInterceptor()
clazz.metaClass.static.invokeMethod = helper.getMethodInterceptor()
clazz.metaClass.methodMissing = helper.getMethodMissingInterceptor()
return clazz
}
}
7 changes: 6 additions & 1 deletion src/main/groovy/com/lesfurets/jenkins/unit/MethodCall.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ import static com.lesfurets.jenkins.unit.MethodSignature.method

import org.codehaus.groovy.runtime.MetaClassHelper

import groovy.transform.CompileStatic

@CompileStatic
class MethodCall {

Object target
Expand Down Expand Up @@ -70,7 +73,9 @@ class MethodCall {

@Override
String toString() {
return "${' ' * (stackDepth)}${target.class.simpleName}.$methodName(${argsToString()})"
return "${' ' * (stackDepth)}" +
"${target instanceof Class ? target.simpleName : target.class.simpleName}." +
"$methodName(${argsToString()})"
}

boolean equals(o) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@ package com.lesfurets.jenkins.unit

import static org.codehaus.groovy.runtime.MetaClassHelper.isAssignableFrom

import groovy.transform.CompileStatic

@CompileStatic
class MethodSignature {
String name
Class[] args
Expand All @@ -16,7 +19,7 @@ class MethodSignature {
}

String argsToString() {
return args.collect {
return args.collect { Class it ->
if (it != null && Closure.isAssignableFrom(it)) {
Closure.class.toString()
} else {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,32 +1,8 @@
package com.lesfurets.jenkins.unit

/**
* This class seems useless now
*/
abstract class MockPipelineScript extends Script {

def methodMissing(String name, args) {
if (this._TEST_HELPER.isMethodAllowed(name, args)) {
def result = null
if (args != null) {
for (argument in args) {
result = callIfClosure(argument, result)
if (argument instanceof Map) {
argument.each { k, v ->
result = callIfClosure(k, result)
result = callIfClosure(v, result)
}
}
}
}
return result
} else {
throw new MissingMethodException(name, this.class, args)
}
}

def callIfClosure(Object closure, Object currentResult) {
if (closure instanceof Closure) {
currentResult = closure.call()
}
return currentResult
}

}
Loading

0 comments on commit fb627ee

Please sign in to comment.