Skip to content

Commit

Permalink
Add unit test and basedeclarativepipelinetest
Browse files Browse the repository at this point in the history
  • Loading branch information
dgoetsch authored and Devyn Goetsch committed Apr 3, 2019
1 parent ae7c20d commit 51efd5f
Show file tree
Hide file tree
Showing 5 changed files with 228 additions and 60 deletions.
22 changes: 22 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -385,6 +385,28 @@ libraryJob.run()
libraryJob.sh(curl -H 'Content-Type: application/json' -X POST -d '{"name" : "Ben"}' http://acme.com)
```

## Declarative Pipeline Support

Declarative pipelines are supported via a test harness. The tests use closures in the file as units of isolation. Effectively,
your tests describe which part of the jenkinsfile your test is verifying and don't interract with the rest of the file. Here is an example that
verifies that sh was invoked during the "init" stage.

```
@Test
void runAStage() {
super.jenkinsfile.run()
harness
.runStage('init')
.runClosureStep('steps')
def call = helper.callStack.find { it.methodName == 'sh' }
assertThat(call.args.toList()).isEqualTo(['echo \'hello world\''])
}
```

There is [a unit test that serves as a complete example](src/test/groovy/java/com/lesfurets/jenkins/TestBaseDeclarativePipelineTest.groovy).


## Note on CPS

If you already fiddled with Jenkins pipeline DSL, you experienced strange errors during execution on Jenkins.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
package com.lesfurets.jenkins.unit

abstract class BaseDeclarativePipelineTest extends BasePipelineTest {

Script jenkinsfile = null

DeclarativePipelineHarness harness


@Override
void setUp() {
if(harness == null || jenkinsfile == null) {
super.setUp()
harness = new DeclarativePipelineHarness(this, allowedClosureSteps(), allowedParameterizedClosureSteps())
loadJenkinsfile()
} else {
harness.clearAllCalls()
}
}

abstract String jenkinsfileName()

def loadJenkinsfile() {
this.jenkinsfile = super.loadScript(jenkinsfileName())
}

List<String> defaultAllowedClosureSteps = ['pipeline',
'options',
'parameters',
'environment',
'triggers',
'tools',
'input',

'agent',
'docker',
'dockerfile',
'node',

'stages',
'parallel',
'script',
'steps',

'when',
'allOf',
'anyOf',
'expression',

'post',
'always',
'cleanup',
'success',
'failure',
'regression',
'changed',
'fixed',
'aborted',
'unstable',
'unsuccessful']

Map<String, Class<Object>> defaultAllowedParameterizedClosureSteps = [
'stage': String.class as Class<Object>,
'node' : String.class as Class<Object>,
'withCredentials': Object.class,
'withEnv': List.class as Class<Object>,
'dir': String.class as Class<Object>,
]

List<String> allowedClosureSteps() {
return defaultAllowedClosureSteps
}

Map<String, Class<Object>> allowedParameterizedClosureSteps() {
return defaultAllowedParameterizedClosureSteps
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,90 +3,82 @@ package com.lesfurets.jenkins.unit
class DeclarativePipelineHarness {
BasePipelineTest base

Map<String, CallStack> callStacks = [:]
Map<String, IdentifiedCallStack<?>> identifiedCallStacks = [:]
Map<String, CallStack> stepCallStacks = [:]
Map<String, ParameterizedCallStack<?>> parameterizedStepCallStacks = [:]

DeclarativePipelineHarness(base) {
Object lastClosureStepParameter = null

List<String> allowedClosureSteps
Map<String, Class<Object>> allowedParameterizedClosureSteps

DeclarativePipelineHarness(BasePipelineTest base, List<String> closureSteps, Map<String, Class<Object>> parameterizedClosureSteps) {
this.base = base
registerCallStack('pipeline')
registerCallStack('options')
registerCallStack('parameters')
registerCallStack('environment')
registerCallStack('triggers')
registerCallStack('tools')
registerCallStack('input')

registerCallStack('agent')
registerCallStack('docker')
registerCallStack('dockerfile')
registerCallStack('node')

registerCallStack('stages')
registerCallStack('parallel')
registerCallStack('script')
registerCallStack('steps')

registerCallStack('when')
registerCallStack('allOf')
registerCallStack('anyOf')
registerCallStack('expression')

//Register post methods
registerCallStack('post')
registerCallStack('always')
registerCallStack('cleanup')
registerCallStack('success')
registerCallStack('failure')
registerCallStack('regression')
registerCallStack('changed')
registerCallStack('fixed')
registerCallStack('aborted')
registerCallStack('unstable')
registerCallStack('unsuccessful')

registerIdentifiedCallStack('stage', String.class)
registerIdentifiedCallStack('node', String.class)
registerIdentifiedCallStack('withCredentials', Object.class)
registerIdentifiedCallStack('withEnv', List.class)
registerIdentifiedCallStack('dir', String.class)
this.allowedClosureSteps = closureSteps
this.allowedParameterizedClosureSteps = parameterizedClosureSteps

allowedClosureSteps.forEach { it -> registerClosureStep(it)}
allowedParameterizedClosureSteps.entrySet().forEach { entry -> registerParameterizedClosureStep(entry.key, entry.value)}
}

void runCall(String name, Integer idx = 0, Boolean doClear = true) {
CallStack callStack = callStacks[name]
DeclarativePipelineHarness runClosureStep(String name, Integer idx = 0, Boolean doClear = true) {
CallStack callStack = stepCallStacks[name]
if(callStack== null) {
throw new RuntimeException("No such call stack ${name}")
}
callStack.run(idx, doClear)
return this
}

public <T> void runIdentified(String name, T value, Boolean doClear = true) {
IdentifiedCallStack<Object> callStack = identifiedCallStacks[name]
public <T> DeclarativePipelineHarness runParameterizedClosureStep(String name, T value, Boolean doClear = true) {
ParameterizedCallStack<Object> callStack = parameterizedStepCallStacks[name]
if(callStack== null) {'pipeline'
throw new RuntimeException("No such call stack ${name}")
}
callStack.run(value, doClear)
this.lastClosureStepParameter = callStack.run(value, doClear)
return this
}

Object runIdentifiedByIndex(String name, Integer idx = 0, Boolean doClear = true) {
IdentifiedCallStack<Object> callStack = identifiedCallStacks[name]
DeclarativePipelineHarness runParameterizedClosureStepByIndex(String name, Integer idx = 0, Boolean doClear = true) {
ParameterizedCallStack<Object> callStack = parameterizedStepCallStacks[name]
if(callStack== null) {
throw new RuntimeException("No such call stack ${name}")
}
return callStack.run(idx, doClear)
this.lastClosureStepParameter = callStack.run(idx, doClear)
return this
}

DeclarativePipelineHarness runStage(String name) {
return this
.runClosureStep('pipeline')
.runClosureStep('stages')
.runParameterizedClosureStep('stage', name)
}

void registerCallStack(String name) {
callStacks.put(name, new CallStack(name, base.helper))
DeclarativePipelineHarness runParallelSubStage(String parentName, String childName) {
return this
.runClosureStep('pipeline')
.runClosureStep('stages')
.runParameterizedClosureStep('stage', parentName)
.runClosureStep('parallel')
.runParameterizedClosureStep('stage', childName)
}

public <T> void registerIdentifiedCallStack(String name, Class<T> clazz) {
identifiedCallStacks.put(name, new IdentifiedCallStack<T>(name, base.helper, clazz))
Object getLastClosureStepParameterValue() {
return lastClosureStepParameter
}

void registerClosureStep(String name) {
stepCallStacks.put(name, new CallStack(name, base.helper))
}

public <T> void registerParameterizedClosureStep(String name, Class<T> clazz) {
parameterizedStepCallStacks.put(name, new ParameterizedCallStack<T>(name, base.helper, clazz))
}

void clearAllCalls() {
base.helper.clearCallStack()
callStacks.values().forEach { it.clearCalls() }
identifiedCallStacks.values().forEach { it.clearCalls() }
stepCallStacks.values().forEach { it.clearCalls() }
parameterizedStepCallStacks.values().forEach { it.clearCalls() }
}


Expand Down Expand Up @@ -117,14 +109,15 @@ class DeclarativePipelineHarness {
}
}

class IdentifiedCallStack<T> {
class ParameterizedCallStack<T> {
String name
PipelineTestHelper helper
Class<T> clazz

List<T> values = []
List<Closure> calls = []
IdentifiedCallStack(name, helper, clazz) {

ParameterizedCallStack(name, helper, clazz) {
this.name = name
this.helper = helper
this.clazz = clazz
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package com.lesfurets.jenkins

import static org.assertj.core.api.Assertions.assertThat;

import com.lesfurets.jenkins.unit.BaseDeclarativePipelineTest
import org.junit.Before
import org.junit.Test

class TestBaseDeclarativePipelineTest extends BaseDeclarativePipelineTest {
@Override
String jenkinsfileName() {
return "src/test/jenkins/job/declarative.jenkins"
}

@Override
@Before
void setUp() {
super.setUp()
}

@Test
void runAStage() {
super.jenkinsfile.run()
harness
.runStage('init')
.runClosureStep('steps')

def call = helper.callStack.find { it.methodName == 'sh' }
assertThat(call.args.toList()).isEqualTo(['echo \'hello world\''])
}

@Test
void runASubStage1() {
super.jenkinsfile.run()
harness
.runParallelSubStage('parallel build', 'build 1')
.runClosureStep('steps')

def call = helper.callStack.find { it.methodName == 'sh' }
assertThat(call.args.toList()).isEqualTo(['echo \'hello build 1\''])
}

@Test
void runASubStage2() {
super.jenkinsfile.run()
harness
.runParallelSubStage('parallel build', 'build 2')
.runClosureStep('steps')

def call = helper.callStack.find { it.methodName == 'sh' }
assertThat(call.args.toList()).isEqualTo(['echo \'hello build 2\''])
}
}
23 changes: 23 additions & 0 deletions src/test/jenkins/job/declarative.jenkins
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
pipeline {
stages {
stage("init") {
steps {
sh "echo 'hello world'"
}
}
stage("parallel build") {
parallel {
stage("build 1") {
steps {
sh "echo 'hello build 1'"
}
}
stage("build 2") {
steps {
sh "echo 'hello build 2'"
}
}
}
}
}
}

0 comments on commit 51efd5f

Please sign in to comment.