Skip to content

Commit

Permalink
Merge pull request #12 from lesfurets/develop
Browse files Browse the repository at this point in the history
Pre release 0.12
  • Loading branch information
ozangunalp authored Mar 17, 2017
2 parents ffba71e + ce7b002 commit 0db0cb4
Show file tree
Hide file tree
Showing 51 changed files with 1,251 additions and 148 deletions.
93 changes: 92 additions & 1 deletion 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('[email protected]: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 All @@ -239,4 +324,10 @@ To simulate this aspect, CPS versions of the helpers transform your scripts into
To use this _*experimental*_ feature, you can use the abstract class `BasePipelineTestCPS` instead of `BasePipelineTest`.
You may see some changes in the call stacks that the helper registers.
Note also that the serialization used to test is not the same as what Jenkins uses.
You may find some incoherence in that level.
You may find some incoherence in that level.

## Contributing

JenkinsPipelineUnit aims to help devops code and test Jenkins pipelines with a shorter development cycle.
It addresses some of the requirements traced in [JENKINS-33925](https://issues.jenkins-ci.org/browse/JENKINS-33925).
If you are willing to contribute please don't hesitate to discuss in issues and open a pull-request.
19 changes: 7 additions & 12 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,15 @@ repositories {

group = "com.lesfurets"
archivesBaseName = "jenkins-pipeline-unit"
version = "0.11"
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
30 changes: 19 additions & 11 deletions src/main/groovy/com/lesfurets/jenkins/unit/BasePipelineTest.groovy
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,18 @@ 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.build()
helper.with {
it.scriptRoots = this.scriptRoots
it.scriptExtension = this.scriptExtension
it.baseClassloader = this.baseClassLoader
it.imports += this.imports
it.baseScriptRoot = this.baseScriptRoot
return it
}.init()

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 All @@ -65,7 +70,7 @@ abstract class BasePipelineTest {
helper.registerAllowedMethod("gatlingArchive", [], null)
helper.registerAllowedMethod("string", [Map.class], stringInterceptor)
helper.registerAllowedMethod("withCredentials", [List.class, Closure.class], withCredentialsInterceptor)
helper.registerAllowedMethod("error", [String.class], { updateBuildStatus('FAILURE')})
helper.registerAllowedMethod("error", [String.class], { updateBuildStatus('FAILURE') })

binding.setVariable('currentBuild', [result: 'SUCCESS'])
}
Expand All @@ -75,11 +80,14 @@ 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
}

Script loadScript(String scriptName) {
if (!helper.isInitialized()) {
throw new IllegalStateException("Helper is not initialized: Call setUp() before tests.")
}
return helper.loadScript(scriptName, this.binding)
}

Expand All @@ -94,7 +102,7 @@ abstract class BasePipelineTest {
/**
* Asserts the job status is FAILURE.
* Please check the mocks update this status when necessary.
* @See #updateBuildStatus(String)
* @See # updateBuildStatus ( String )
*/
void assertJobStatusFailure() {
assertJobStatus('FAILURE')
Expand All @@ -103,7 +111,7 @@ abstract class BasePipelineTest {
/**
* Asserts the job status is UNSTABLE.
* Please check the mocks update this status when necessary
* @See #updateBuildStatus(String)
* @See # updateBuildStatus ( String )
*/
void assertJobStatusUnstable() {
assertJobStatus('UNSTABLE')
Expand All @@ -112,7 +120,7 @@ abstract class BasePipelineTest {
/**
* Asserts the job status is SUCCESS.
* Please check the mocks update this status when necessary
* @See #updateBuildStatus(String)
* @See # updateBuildStatus ( String )
*/
void assertJobStatusSuccess() {
assertJobStatus('SUCCESS')
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 0db0cb4

Please sign in to comment.