Skip to content

Commit

Permalink
Add support for global vars and steps
Browse files Browse the repository at this point in the history
  • Loading branch information
ozangunalp committed Mar 2, 2017
1 parent a317fb5 commit b196fcf
Show file tree
Hide file tree
Showing 12 changed files with 157 additions and 20 deletions.
3 changes: 2 additions & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@ 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'
testCompile group: 'junit', name: 'junit', version: '4.11'
compile group: 'commons-io', name: 'commons-io', version: '2.5'
testCompile group: 'junit', name: 'junit', version: '4.11'
}

// Get external properties from file
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import java.util.function.Function

import org.codehaus.groovy.control.CompilerConfiguration
import org.codehaus.groovy.control.customizers.ImportCustomizer
import org.codehaus.groovy.runtime.InvokerHelper
import org.codehaus.groovy.runtime.MetaClassHelper

import com.lesfurets.jenkins.unit.global.lib.LibraryConfiguration
Expand Down Expand Up @@ -58,7 +59,9 @@ class PipelineTestHelper {
*/
List<MethodCall> callStack = []

private GroovyScriptEngine gse
protected GroovyScriptEngine gse

protected LibraryLoader libLoader

/**
* Method interceptor for method 'load' to load scripts via encapsulated GroovyScriptEngine
Expand Down Expand Up @@ -149,8 +152,8 @@ class PipelineTestHelper {
CompilerConfiguration configuration = new CompilerConfiguration()
GroovyClassLoader cLoader = new GroovyClassLoader(baseClassloader, configuration)

LibraryLoader libraryLoader = new LibraryLoader(cLoader, libraries)
LibraryTransformer libraryTransformer = new LibraryTransformer(libraryLoader)
libLoader = new LibraryLoader(cLoader, libraries)
LibraryTransformer libraryTransformer = new LibraryTransformer(libLoader)
configuration.addCompilationCustomizers(libraryTransformer)

ImportCustomizer importCustomizer = new ImportCustomizer()
Expand Down Expand Up @@ -213,7 +216,9 @@ class PipelineTestHelper {
Script loadScript(String scriptName, Binding binding) {
Objects.requireNonNull(binding)
binding.setVariable("_TEST_HELPER", this)
Script script = gse.createScript(scriptName, binding)
Class scriptClass = gse.loadScriptByName(scriptName)
libLoader.setGlobalVars(binding, this)
Script script = InvokerHelper.createScript(scriptClass, binding)
script.metaClass.invokeMethod = methodInterceptor
script.metaClass.static.invokeMethod = methodInterceptor
script.run()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,18 @@ package com.lesfurets.jenkins.unit.cps

import org.codehaus.groovy.control.CompilerConfiguration
import org.codehaus.groovy.control.customizers.ImportCustomizer
import org.codehaus.groovy.runtime.InvokerHelper

import com.cloudbees.groovy.cps.*
import com.cloudbees.groovy.cps.Continuation
import com.cloudbees.groovy.cps.ObjectInputStreamWithLoader
import com.cloudbees.groovy.cps.impl.CpsCallableInvocation
import com.lesfurets.jenkins.unit.PipelineTestHelper
import com.lesfurets.jenkins.unit.global.lib.LibraryLoader
import com.lesfurets.jenkins.unit.global.lib.LibraryTransformer
import com.lesfurets.jenkins.unit.global.lib.*

class PipelineTestHelperCPS extends PipelineTestHelper {

protected Class scriptBaseClass = MockPipelineScriptCPS.class

private GroovyScriptEngine gse

protected parallelInterceptor = { Map m ->
// If you have many steps in parallel and one of the step in Jenkins fails, the other tasks keep runnning in Jenkins.
// Since here the parallel steps are executed sequentially, we are hiding the error to let other steps run
Expand Down Expand Up @@ -74,15 +73,15 @@ class PipelineTestHelperCPS extends PipelineTestHelper {
CompilerConfiguration configuration = new CompilerConfiguration()
GroovyClassLoader cLoader = new GroovyClassLoader(baseClassloader, configuration)

LibraryLoader libraryLoader = new LibraryLoader(cLoader, libraries)
LibraryTransformer libraryTransformer = new LibraryTransformer(libraryLoader)
libLoader = new LibraryLoader(cLoader, libraries)
LibraryTransformer libraryTransformer = new LibraryTransformer(libLoader)
configuration.addCompilationCustomizers(libraryTransformer)

ImportCustomizer importCustomizer = new ImportCustomizer()
imports.each { k, v -> importCustomizer.addImport(k, v) }
configuration.addCompilationCustomizers(importCustomizer)
// Add transformer for CPS compilation
configuration.addCompilationCustomizers(new CpsTransformer())
configuration.addCompilationCustomizers(new LibraryCpsTransformer())

configuration.setDefaultScriptExtension(scriptExtension)
configuration.setScriptBaseClass(scriptBaseClass.getName())
Expand All @@ -102,7 +101,9 @@ class PipelineTestHelperCPS extends PipelineTestHelper {
Script loadScript(String scriptName, Binding binding) {
Objects.requireNonNull(binding)
binding.setVariable("_TEST_HELPER", this)
Script script = gse.createScript(scriptName, binding)
Class scriptClass = gse.loadScriptByName(scriptName)
libLoader.setGlobalVars(binding, this)
Script script = InvokerHelper.createScript(scriptClass, binding)
script.metaClass.invokeMethod = methodInterceptor
script.metaClass.static.invokeMethod = methodInterceptor
// Probably unnecessary
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
package com.lesfurets.jenkins.unit.global.lib

import groovy.transform.ToString
import groovy.transform.builder.Builder

/**
* Mock for org.jenkinsci.plugins.workflow.libs.LibraryConfiguration
*/
@Builder(builderMethodName = "library")
@ToString
class LibraryConfiguration {

String name
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package com.lesfurets.jenkins.unit.global.lib

import org.codehaus.groovy.ast.ClassNode
import org.codehaus.groovy.ast.MethodNode
import org.codehaus.groovy.classgen.GeneratorContext
import org.codehaus.groovy.control.SourceUnit

import com.cloudbees.groovy.cps.CpsTransformer

class LibraryCpsTransformer extends CpsTransformer {

boolean skipTransform = false

@Override
void call(SourceUnit source, GeneratorContext context, ClassNode classNode) {
boolean f = source.name.contains('/vars/')
skipTransform = f
super.call(source, context, classNode)
skipTransform = false
}

@Override
protected boolean shouldBeTransformed(MethodNode node) {
return !skipTransform && super.shouldBeTransformed(node)
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
package com.lesfurets.jenkins.unit.global.lib

import static com.lesfurets.jenkins.unit.MethodSignature.method

import java.nio.file.Files

import org.apache.commons.io.FilenameUtils

import com.lesfurets.jenkins.unit.PipelineTestHelper

/**
* Loads libraries to groovy class loader
*/
Expand All @@ -9,6 +17,8 @@ class LibraryLoader {

private final Set<String> loadedLibraries = new HashSet<>()

private final Set<LibraryRecord> libRecords = new HashSet<>()

private final Map<String, LibraryConfiguration> libraryDescriptions

LibraryLoader(GroovyClassLoader groovyClassLoader, Map<String, LibraryConfiguration> libraryDescriptions) {
Expand All @@ -25,10 +35,14 @@ class LibraryLoader {
.filter { !loadedLibraries.contains(getExpression(it)) }
.forEach {
doLoadLibrary(it)
loadedLibraries.add(getExpression(it))
}
}

/**
* Load library described by expression if it corresponds to a known library configuration
* @param expression
* @throws Exception
*/
void loadLibrary(String expression) throws Exception {
def lib = parse(expression)
def libName = lib[0]
Expand All @@ -43,7 +57,28 @@ class LibraryLoader {
def loadedLib = getExpression(library, version)
if (!loadedLibraries.contains(loadedLib)) {
doLoadLibrary(library, version)
loadedLibraries.add(loadedLib)
}
}

/**
* Sets global variables defined in loaded libraries on the binding
* @param binding
*/
void setGlobalVars(Binding binding, PipelineTestHelper helper) {
libRecords.stream()
.flatMap { it.definedGlobalVars.entrySet().stream() }
.forEach { e ->
if (e.value instanceof Script) {
def script = Script.cast(e.value)
script.setBinding(binding)
script.metaClass.invokeMethod = helper.methodInterceptor
script.metaClass.static.invokeMethod = helper.methodInterceptor
e.value.metaClass.getMethods().findAll { it.name == 'call' }.forEach { m ->
helper.registerAllowedMethod(method(e.value.class.name, m.getNativeParameterTypes()),
{ args -> m.doMethodInvoke(e.value, args) })
}
}
binding.setVariable(e.key, e.value)
}
}

Expand All @@ -56,14 +91,35 @@ class LibraryLoader {
*/
private void doLoadLibrary(LibraryConfiguration library, String version = null) throws Exception {
println "Loading shared library ${library.name} with version ${version ?: library.defaultVersion}"
loadedLibraries.add(getExpression(library, version))
try {
def urls = library.retriever.retrieve(library.name, version ?: library.defaultVersion, library.targetPath)
def globalVars = [:]
urls.forEach { url ->
def file = new File(url.toURI())
groovyClassLoader.addURL(file.toPath().resolve('src').toUri().toURL())

def srcPath = file.toPath().resolve('src')
def varsPath = file.toPath().resolve('vars')
groovyClassLoader.addURL(srcPath.toUri().toURL())
groovyClassLoader.addURL(varsPath.toUri().toURL())

if (varsPath.toFile().exists()) {
Files.list(varsPath)
.map { it.toFile() }
.filter { it.name.endsWith('.groovy') }
.map { FilenameUtils.getBaseName(it.name) }
.filter { !globalVars.containsValue(it) }
.forEach {
globalVars.put(it, groovyClassLoader.loadClass(it).newInstance())
}
}
}
libRecords.add(new LibraryRecord([configuration : library,
version : version ?: library.defaultVersion,
definedGlobalVars: globalVars
]))
} catch (Throwable t) {
throw t
throw new Exception(t.message, t)
}
}

Expand Down Expand Up @@ -92,5 +148,4 @@ class LibraryLoader {
}
return false
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.lesfurets.jenkins.unit.global.lib

import groovy.transform.Canonical

@Canonical
class LibraryRecord {

LibraryConfiguration configuration
String version
Map<String, Object> definedGlobalVars

String getIdentifier() {
return "$configuration.name@$version"
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,11 @@ class LibraryTransformer extends CompilationCustomizer {
final Map<String, AnnotationNode> libraryAnnotations = new HashMap<>()

// load implicit libraries
libraryLoader.loadImplicitLibraries()
try {
libraryLoader.loadImplicitLibraries()
} catch (Exception e) {
source.addError(new SyntaxException(e.getMessage(), 0, 0))
}

new ClassCodeVisitorSupport() {

Expand Down
7 changes: 7 additions & 0 deletions src/main/jenkins/job/library/libraryJob.jenkins
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,13 @@ package job.library
@Library('commons')
import net.courtanet.jenkins.Utils

sh acme.name
acme.name = 'something'
sh acme.name

sayHello 'World'
sayHello()

def execute() {
parallel(
action1: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import org.junit.runners.Parameterized
import org.junit.runners.Parameterized.Parameter
import org.junit.runners.Parameterized.Parameters

import com.lesfurets.jenkins.unit.BasePipelineTest
import com.lesfurets.jenkins.unit.cps.BasePipelineTestCPS

@RunWith(Parameterized.class)
Expand Down
13 changes: 13 additions & 0 deletions src/test/resources/shared-scripts/commons@master/vars/acme.groovy
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
// vars/acme.groovy
class acme implements Serializable {
private String name = "something"
def setName(value) {
name = value
}
def getName() {
name
}
def caution(message) {
echo "Hello, ${name}! CAUTION: ${message}"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
// vars/sayHello.groovy
def call(String name = 'human') {
// Any valid steps can be called from this code, just like in other
// Scripted Pipeline
echo "Hello, ${name}."
}

0 comments on commit b196fcf

Please sign in to comment.