diff --git a/pom.xml b/pom.xml index cab1ae1e1..7675703ed 100644 --- a/pom.xml +++ b/pom.xml @@ -109,6 +109,26 @@ 1.19 test + + + org.springframework + spring-context + 4.3.12.RELEASE + test + + + org.springframework + spring-context-support + 4.3.10.RELEASE + test + + + org.springframework + spring-test + 4.3.5.RELEASE + test + + src diff --git a/resources/default_pipeline_environment.yml b/resources/default_pipeline_environment.yml index 462488714..49167e31b 100644 --- a/resources/default_pipeline_environment.yml +++ b/resources/default_pipeline_environment.yml @@ -14,18 +14,6 @@ steps: docker: filePath: 'Dockerfile' versioningTemplate: '${version}-${timestamp}${commitId?"_"+commitId:""}' - mavenExecute: - dockerImage: 'maven:3.5-jdk-7' - influxWriteData: - influxServer: 'jenkins' - mtaBuild: - buildTarget: 'NEO' - mtaJarLocation: 'mta.jar' - neoDeploy: - deployMode: 'mta' - warAction: 'deploy' - vmSize: 'lite' - neoCredentialsId: 'CI_CREDENTIALS_ID' checksPublishResults: aggregation: active: true @@ -85,6 +73,48 @@ steps: fail: high: '0' archive: false + influxWriteData: + influxServer: 'jenkins' + mavenExecute: + dockerImage: 'maven:3.5-jdk-7' + mtaBuild: + buildTarget: 'NEO' + mtaJarLocation: 'mta.jar' + neoDeploy: + deployMode: 'mta' + warAction: 'deploy' + vmSize: 'lite' + neoCredentialsId: 'CI_CREDENTIALS_ID' + pipelineStashFilesAfterBuild: + stashIncludes: + checkmarx: '**/*.js, **/*.scala, **/*.py, **/*.go, **/*.xml, **/*.html' + classFiles: '**/target/classes/**/*.class, **/target/test-classes/**/*.class' + sonar: '**/jacoco*.exec, **/sonar-project.properties' + stashExcludes: + checkmarx: '**/*.mockserver.js, node_modules/**/*.js' + classFiles: '' + sonar: '' + pipelineStashFilesBeforeBuild: + stashIncludes: + buildDescriptor: '**/pom.xml, **/.mvn/**, **/assembly.xml, **/.swagger-codegen-ignore, **/package.json, **/requirements.txt, **/setup.py, **/whitesource_config.py, **/mta*.y*ml, **/.npmrc, **/whitesource.*.json, **/whitesource-fs-agent.config, .xmake.cfg, Dockerfile, **/VERSION, **/version.txt, **/build.sbt, **/sbtDescriptor.json, **/project/*' + deployDescriptor: '**/manifest*.y*ml, **/*.mtaext.y*ml, **/*.mtaext, **/xs-app.json, helm/**, *.y*ml' + git: '**/gitmetadata/**' + opa5: '**/*.*' + 'opensource configuration': '**/srcclr.yml, **/vulas-custom.properties, **/.nsprc, **/.retireignore, **/.retireignore.json, **/' + pipelineConfigAndTests: '.pipeline/*.*' + securityDescriptor: '**/xs-security.json' + 'snyk configuration': '**/.snyk' + tests: '**/pom.xml, **/*.json, **/*.xml, **/src/**, **/node_modules/**, **/specs/**, **/env/**, **/*.js' + stashExcludes: + buildDescriptor: '**/node_modules/**/package.json' + deployDescriptor: '' + git: '' + opa5: '' + 'opensource configuration': '' + pipelineConfigAndTests: '' + securityDescriptor: '' + 'snyk configuration': '' + tests: '' testsPublishResults: junit: pattern: '**/target/surefire-reports/*.xml' diff --git a/src/com/sap/piper/Utils.groovy b/src/com/sap/piper/Utils.groovy index 5b6792db0..11221baf9 100644 --- a/src/com/sap/piper/Utils.groovy +++ b/src/com/sap/piper/Utils.groovy @@ -15,3 +15,39 @@ def getMandatoryParameter(Map map, paramName, defaultValue = null) { return paramValue } + +def stash(name, include = '**/*.*', exclude = '') { + echo "Stash content: ${name} (include: ${include}, exclude: ${exclude})" + steps.stash name: name, includes: include, excludes: exclude +} + +def stashWithMessage(name, msg, include = '**/*.*', exclude = '') { + try { + stash(name, include, exclude) + } catch (e) { + echo msg + name + " (${e.getMessage()})" + } +} + +def unstash(name, msg = "Unstash failed:") { + def unstashedContent = [] + try { + echo "Unstash content: ${name}" + steps.unstash name + unstashedContent += name + } catch (e) { + echo "$msg $name (${e.getMessage()})" + } + return unstashedContent +} + +def unstashAll(stashContent) { + def unstashedContent = [] + if (stashContent) { + for (i = 0; i < stashContent.size(); i++) { + unstashedContent += unstash(stashContent[i]) + } + } + return unstashedContent +} + diff --git a/test/groovy/InfluxWriteDataTest.groovy b/test/groovy/InfluxWriteDataTest.groovy index 23d693895..95dd03516 100644 --- a/test/groovy/InfluxWriteDataTest.groovy +++ b/test/groovy/InfluxWriteDataTest.groovy @@ -63,7 +63,7 @@ class InfluxWriteDataTest extends BasePipelineTest { assertEquals('testInflux', stepMap.selectedTarget) assertEquals(null, stepMap.customPrefix) assertEquals([:], stepMap.customData) - assertEquals([pipeline_data:[:]], stepMap.customDataMap) + assertEquals([pipeline_data: [:], step_data: [:]], stepMap.customDataMap) assertTrue(fileMap.containsKey('jenkins_data.json')) assertTrue(fileMap.containsKey('pipeline_data.json')) diff --git a/test/groovy/MavenExecuteTest.groovy b/test/groovy/MavenExecuteTest.groovy index 7318190fb..cd80de9d4 100644 --- a/test/groovy/MavenExecuteTest.groovy +++ b/test/groovy/MavenExecuteTest.groovy @@ -9,10 +9,11 @@ import com.lesfurets.jenkins.unit.BasePipelineTest import static org.junit.Assert.assertEquals import static org.junit.Assert.assertTrue +import util.BasePiperTest import util.JenkinsShellCallRule import util.Rules -class MavenExecuteTest extends BasePipelineTest { +class MavenExecuteTest extends BasePiperTest { Map dockerParameters @@ -23,7 +24,6 @@ class MavenExecuteTest extends BasePipelineTest { .around(jscr) def mavenExecuteScript - def cpe @Before void init() { @@ -37,13 +37,12 @@ class MavenExecuteTest extends BasePipelineTest { }) mavenExecuteScript = loadScript("mavenExecute.groovy").mavenExecute - cpe = loadScript('commonPipelineEnvironment.groovy').commonPipelineEnvironment } @Test void testExecuteBasicMavenCommand() throws Exception { - mavenExecuteScript.call(script: [commonPipelineEnvironment: cpe], goals: 'clean install') + mavenExecuteScript.call(script: nullScript, goals: 'clean install') assertEquals('maven:3.5-jdk-7', dockerParameters.dockerImage) assert jscr.shell[0] == 'mvn clean install' @@ -53,7 +52,7 @@ class MavenExecuteTest extends BasePipelineTest { void testExecuteMavenCommandWithParameter() throws Exception { mavenExecuteScript.call( - script: [commonPipelineEnvironment: cpe], + script: nullScript, dockerImage: 'maven:3.5-jdk-8-alpine', goals: 'clean install', globalSettingsFile: 'globalSettingsFile.xml', @@ -70,7 +69,7 @@ class MavenExecuteTest extends BasePipelineTest { @Test void testMavenCommandForwardsDockerOptions() throws Exception { - mavenExecuteScript.call(script: [commonPipelineEnvironment: cpe], goals: 'clean install') + mavenExecuteScript.call(script: nullScript, goals: 'clean install') assertEquals('maven:3.5-jdk-7', dockerParameters.dockerImage) assert jscr.shell[0] == 'mvn clean install' diff --git a/test/groovy/PipelineStashFilesAfterBuildTest.groovy b/test/groovy/PipelineStashFilesAfterBuildTest.groovy new file mode 100644 index 000000000..86ea41185 --- /dev/null +++ b/test/groovy/PipelineStashFilesAfterBuildTest.groovy @@ -0,0 +1,72 @@ +import org.junit.Rule +import org.junit.Test +import org.junit.rules.RuleChain +import util.* + +import static org.hamcrest.Matchers.containsString +import static org.junit.Assert.assertFalse +import static org.junit.Assert.assertThat + +class PipelineStashFilesAfterBuildTest extends BasePiperTest { + JenkinsStepRule jsr = new JenkinsStepRule(this) + JenkinsLoggingRule jlr = new JenkinsLoggingRule(this) + JenkinsReadJsonRule jrj = new JenkinsReadJsonRule(this) + + @Rule + public RuleChain rules = Rules + .getCommonRules(this) + .around(jrj) + .around(jlr) + .around(jsr) + + @Test + void testStashAfterBuild() { + helper.registerAllowedMethod("fileExists", [String.class], { + searchTerm -> + return false + }) + jsr.step.call( + script: nullScript, + juStabUtils: utils + ) + // asserts + assertFalse(jlr.log.contains('Stash content: checkmarx')) + assertThat(jlr.log, containsString('Stash content: classFiles (include: **/target/classes/**/*.class, **/target/test-classes/**/*.class, exclude: )')) + assertThat(jlr.log, containsString('Stash content: sonar (include: **/jacoco*.exec, **/sonar-project.properties, exclude: )')) + assertFalse(jlr.log.contains('Stash content: postStagedFiles')) + } + + @Test + void testStashAfterBuildWithCheckmarx() { + helper.registerAllowedMethod("fileExists", [String.class], { + searchTerm -> + return true + }) + jsr.step.call( + script: nullScript, + juStabUtils: utils, + runCheckmarx: true + ) + // asserts + assertThat(jlr.log, containsString('Stash content: checkmarx (include: **/*.js, **/*.scala, **/*.py, **/*.go, **/*.xml, **/*.html, exclude: **/*.mockserver.js, node_modules/**/*.js)')) + assertThat(jlr.log, containsString('Stash content: classFiles (include: **/target/classes/**/*.class, **/target/test-classes/**/*.class, exclude: )')) + assertThat(jlr.log, containsString('Stash content: sonar (include: **/jacoco*.exec, **/sonar-project.properties, exclude: )')) + } + + @Test + void testStashAfterBuildWithCheckmarxConfig() { + helper.registerAllowedMethod("fileExists", [String.class], { + searchTerm -> + return true + }) + jsr.step.call( + script: [commonPipelineEnvironment: [configuration: [steps: [executeCheckmarxScan: [checkmarxProject: 'TestProject']]]]], + juStabUtils: utils, + ) + // asserts + assertThat(jlr.log, containsString('Stash content: checkmarx (include: **/*.js, **/*.scala, **/*.py, **/*.go, **/*.xml, **/*.html, exclude: **/*.mockserver.js, node_modules/**/*.js)')) + assertThat(jlr.log, containsString('Stash content: classFiles (include: **/target/classes/**/*.class, **/target/test-classes/**/*.class, exclude: )')) + assertThat(jlr.log, containsString('Stash content: sonar (include: **/jacoco*.exec, **/sonar-project.properties, exclude: )')) + } + +} diff --git a/test/groovy/PipelineStashFilesBeforeBuildTest.groovy b/test/groovy/PipelineStashFilesBeforeBuildTest.groovy new file mode 100644 index 000000000..e93a3963a --- /dev/null +++ b/test/groovy/PipelineStashFilesBeforeBuildTest.groovy @@ -0,0 +1,76 @@ +import org.junit.Rule +import org.junit.Test +import org.junit.rules.RuleChain +import util.* + +import static org.hamcrest.Matchers.containsString +import static org.junit.Assert.* + +class PipelineStashFilesBeforeBuildTest extends BasePiperTest { + JenkinsStepRule jsr = new JenkinsStepRule(this) + JenkinsLoggingRule jlr = new JenkinsLoggingRule(this) + JenkinsShellCallRule jscr = new JenkinsShellCallRule(this) + //JenkinsReadJsonRule jrj = new JenkinsReadJsonRule(this) + + @Rule + public RuleChain rules = Rules + .getCommonRules(this) + //.around(jrj) + .around(jlr) + .around(jscr) + .around(jsr) + + @Test + void testStashBeforeBuildNoOpa() { + + jsr.step.call(script: nullScript, juStabUtils: utils) + + // asserts + assertEquals('mkdir -p gitmetadata', jscr.shell[0]) + assertEquals('cp -rf .git/* gitmetadata', jscr.shell[1]) + assertEquals('chmod -R u+w gitmetadata', jscr.shell[2]) + assertFalse(jlr.log.contains('Stash content: opa5')) + assertThat(jlr.log, containsString('Stash content: git (include: **/gitmetadata/**, exclude: )')) + assertThat(jlr.log, containsString('Stash content: tests (include: **/pom.xml, **/*.json, **/*.xml, **/src/**, **/node_modules/**, **/specs/**, **/env/**, **/*.js, exclude: )')) + assertThat(jlr.log, containsString('Stash content: buildDescriptor (include: **/pom.xml, **/.mvn/**, **/assembly.xml, **/.swagger-codegen-ignore, **/package.json, **/requirements.txt, **/setup.py, **/whitesource_config.py, **/mta*.y*ml, **/.npmrc, **/whitesource.*.json, **/whitesource-fs-agent.config, .xmake.cfg, Dockerfile, **/VERSION, **/version.txt, **/build.sbt, **/sbtDescriptor.json, **/project/*, exclude: **/node_modules/**/package.json)')) + assertThat(jlr.log, containsString('Stash content: deployDescriptor (include: **/manifest*.y*ml, **/*.mtaext.y*ml, **/*.mtaext, **/xs-app.json, helm/**, *.y*ml, exclude: )')) + assertThat(jlr.log, containsString('Stash content: opensource configuration (include: **/srcclr.yml')) + assertThat(jlr.log, containsString('Stash content: snyk configuration (include: **/.snyk')) + assertThat(jlr.log, containsString('Stash content: pipelineConfigAndTests (include: .pipeline/*.*')) + assertThat(jlr.log, containsString('Stash content: securityDescriptor (include: **/xs-security.json')) + } + + @Test + void testStashBeforeBuildOpa() { + + jsr.step.call(script: nullScript, juStabUtils: utils, runOpaTests: true) + + // asserts + assertThat(jlr.log, containsString('Stash content: opa5')) + assertThat(jlr.log, containsString('Stash content: git')) + assertThat(jlr.log, containsString('Stash content: tests')) + assertThat(jlr.log, containsString('Stash content: buildDescriptor')) + assertThat(jlr.log, containsString('Stash content: deployDescriptor')) + assertThat(jlr.log, containsString('Stash content: opensource configuration')) + assertThat(jlr.log, containsString('Stash content: snyk configuration')) + assertThat(jlr.log, containsString('Stash content: pipelineConfigAndTests')) + assertThat(jlr.log, containsString('Stash content: securityDescriptor')) + } + + @Test + void testStashBeforeBuildOpaCompatibility() { + + jsr.step.call(script: nullScript, juStabUtils: utils, runOpaTests: 'true') + + // asserts + assertThat(jlr.log, containsString('Stash content: opa5')) + assertThat(jlr.log, containsString('Stash content: git')) + assertThat(jlr.log, containsString('Stash content: tests')) + assertThat(jlr.log, containsString('Stash content: buildDescriptor')) + assertThat(jlr.log, containsString('Stash content: deployDescriptor')) + assertThat(jlr.log, containsString('Stash content: opensource configuration')) + assertThat(jlr.log, containsString('Stash content: snyk configuration')) + assertThat(jlr.log, containsString('Stash content: pipelineConfigAndTests')) + assertThat(jlr.log, containsString('Stash content: securityDescriptor')) + } +} diff --git a/test/groovy/com/sap/piper/versioning/MavenArtifactVersioningTest.groovy b/test/groovy/com/sap/piper/versioning/MavenArtifactVersioningTest.groovy index 19068baf3..aec4c810a 100644 --- a/test/groovy/com/sap/piper/versioning/MavenArtifactVersioningTest.groovy +++ b/test/groovy/com/sap/piper/versioning/MavenArtifactVersioningTest.groovy @@ -6,13 +6,14 @@ import org.junit.Rule import org.junit.Test import org.junit.rules.ExpectedException import org.junit.rules.RuleChain +import util.BasePiperTest import util.JenkinsReadMavenPomRule import util.JenkinsShellCallRule import util.Rules import static org.junit.Assert.assertEquals -class MavenArtifactVersioningTest extends BasePipelineTest{ +class MavenArtifactVersioningTest extends BasePiperTest{ Map dockerParameters def mavenExecuteScript @@ -21,10 +22,12 @@ class MavenArtifactVersioningTest extends BasePipelineTest{ MavenArtifactVersioning av JenkinsShellCallRule jscr = new JenkinsShellCallRule(this) - ExpectedException thrown = ExpectedException.none() @Rule - public RuleChain ruleChain = Rules.getCommonRules(this).around(jscr).around(thrown).around(new JenkinsReadMavenPomRule(this, 'test/resources/MavenArtifactVersioning')) + public RuleChain ruleChain = Rules + .getCommonRules(this) + .around(jscr) + .around(new JenkinsReadMavenPomRule(this, 'test/resources/MavenArtifactVersioning')) @Before void init() { @@ -35,16 +38,11 @@ class MavenArtifactVersioningTest extends BasePipelineTest{ dockerParameters = parameters closure() }) - - mavenExecuteScript = loadScript("mavenExecute.groovy").mavenExecute - commonPipelineEnvironment = loadScript('commonPipelineEnvironment.groovy').commonPipelineEnvironment - - prepareObjectInterceptors(this) } @Test void testVersioning() { - av = new MavenArtifactVersioning(this, [filePath: 'pom.xml']) + av = new MavenArtifactVersioning(nullScript, [filePath: 'pom.xml']) assertEquals('1.2.3', av.getVersion()) av.setVersion('1.2.3-20180101') assertEquals('mvn --file \'pom.xml\' versions:set -DnewVersion=1.2.3-20180101', jscr.shell[0]) @@ -52,15 +50,9 @@ class MavenArtifactVersioningTest extends BasePipelineTest{ @Test void testVersioningCustomFilePathSnapshot() { - av = new MavenArtifactVersioning(this, [filePath: 'snapshot/pom.xml']) + av = new MavenArtifactVersioning(nullScript, [filePath: 'snapshot/pom.xml']) assertEquals('1.2.3', av.getVersion()) av.setVersion('1.2.3-20180101') assertEquals('mvn --file \'snapshot/pom.xml\' versions:set -DnewVersion=1.2.3-20180101', jscr.shell[0]) } - - void prepareObjectInterceptors(object) { - object.metaClass.invokeMethod = helper.getMethodInterceptor() - object.metaClass.static.invokeMethod = helper.getMethodInterceptor() - object.metaClass.methodMissing = helper.getMethodMissingInterceptor() - } } diff --git a/test/groovy/util/BasePiperTest.groovy b/test/groovy/util/BasePiperTest.groovy new file mode 100644 index 000000000..e90da1c35 --- /dev/null +++ b/test/groovy/util/BasePiperTest.groovy @@ -0,0 +1,52 @@ +#!groovy + +package util + +import com.lesfurets.jenkins.unit.BasePipelineTest +import com.sap.piper.Utils +import org.junit.Before +import org.junit.runner.RunWith +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.test.context.ContextConfiguration +import org.springframework.test.context.TestExecutionListeners +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner + +@RunWith(SpringJUnit4ClassRunner) +@ContextConfiguration(classes = [BasePiperTestContext.class]) +@TestExecutionListeners(listeners = [LibraryLoadingTestExecutionListener.class], mergeMode = TestExecutionListeners.MergeMode.MERGE_WITH_DEFAULTS) +abstract class BasePiperTest extends BasePipelineTest { + + @Autowired + MockHelper mockHelper + + @Autowired + Script nullScript + + @Autowired + Utils utils + + @Override + @Before + void setUp() throws Exception { + helper = LibraryLoadingTestExecutionListener.singletonInstance + if(!isHelperInitialized()) { + super.setScriptRoots('.', 'vars') + super.setUp() + } + } + + boolean isHelperInitialized() { + try { + helper.loadScript('dummy.groovy') + } catch (Exception e) { + if (e.getMessage().startsWith('GroovyScriptEngine is not initialized:')) + return false + } + return true + } + + @Deprecated + void prepareObjectInterceptors(Object object) { + LibraryLoadingTestExecutionListener.prepareObjectInterceptors(object) + } +} diff --git a/test/groovy/util/BasePiperTestContext.groovy b/test/groovy/util/BasePiperTestContext.groovy new file mode 100644 index 000000000..6f840bfdd --- /dev/null +++ b/test/groovy/util/BasePiperTestContext.groovy @@ -0,0 +1,37 @@ +#!groovy + +package util + +import com.sap.piper.Utils +import org.codehaus.groovy.runtime.InvokerHelper +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration + +@Configuration +class BasePiperTestContext { + + @Bean + Script nullScript() { + def nullScript = InvokerHelper.createScript(null, new Binding()) + nullScript.currentBuild = [:] + LibraryLoadingTestExecutionListener.prepareObjectInterceptors(nullScript) + return nullScript + } + + @Bean + Utils mockUtils() { + def mockUtils = new Utils() + mockUtils.steps = [ + stash : { m -> println "stash name = ${m.name}" }, + unstash: { println "unstash called ..." } + ] + LibraryLoadingTestExecutionListener.prepareObjectInterceptors(mockUtils) + return mockUtils + } + + @Bean + MockHelper mockHelper() { + return new MockHelper() + } + +} diff --git a/test/groovy/util/JenkinsEnvironmentRule.groovy b/test/groovy/util/JenkinsEnvironmentRule.groovy index 00cd9c841..6944f4455 100644 --- a/test/groovy/util/JenkinsEnvironmentRule.groovy +++ b/test/groovy/util/JenkinsEnvironmentRule.groovy @@ -21,6 +21,11 @@ class JenkinsEnvironmentRule implements TestRule { @Override void evaluate() throws Throwable { env = testInstance.loadScript('commonPipelineEnvironment.groovy').commonPipelineEnvironment + try { + testInstance?.nullScript.commonPipelineEnvironment = env + } catch (MissingPropertyException e) { + //kept for backward compatibility before all tests inherit from BasePiperTest + } base.evaluate() } } diff --git a/test/groovy/util/JenkinsReadJsonRule.groovy b/test/groovy/util/JenkinsReadJsonRule.groovy new file mode 100644 index 000000000..58a6f8ab6 --- /dev/null +++ b/test/groovy/util/JenkinsReadJsonRule.groovy @@ -0,0 +1,45 @@ +package util + +import com.lesfurets.jenkins.unit.BasePipelineTest +import groovy.json.JsonSlurper +import org.junit.rules.TestRule +import org.junit.runner.Description +import org.junit.runners.model.Statement + +class JenkinsReadJsonRule implements TestRule { + + final BasePipelineTest testInstance + final String testRoot + + JenkinsReadJsonRule(BasePipelineTest testInstance, testRoot = '') { + this.testInstance = testInstance + this.testRoot = testRoot + } + + @Override + Statement apply(Statement base, Description description) { + return statement(base) + } + + private Statement statement(final Statement base) { + return new Statement() { + @Override + void evaluate() throws Throwable { + testInstance.helper.registerAllowedMethod("readJSON", [Map], { Map m -> + if(m.text) { + def js = new JsonSlurper() + return js.parseText(m.text) + } else if(m.file) { + def js = new JsonSlurper() + def reader = new BufferedReader(new FileReader( "${this.testRoot}${m.file}" )) + return js.parse(reader) + } else { + throw new IllegalArgumentException("Key 'text' is missing in map ${m}.") + } + }) + + base.evaluate() + } + } + } +} diff --git a/test/groovy/util/LibraryLoadingTestExecutionListener.groovy b/test/groovy/util/LibraryLoadingTestExecutionListener.groovy new file mode 100644 index 000000000..d8dd628f6 --- /dev/null +++ b/test/groovy/util/LibraryLoadingTestExecutionListener.groovy @@ -0,0 +1,253 @@ +package util + +import com.lesfurets.jenkins.unit.InterceptingGCL +import com.lesfurets.jenkins.unit.MethodSignature +import com.lesfurets.jenkins.unit.PipelineTestHelper +import org.codehaus.groovy.control.CompilerConfiguration +import org.springframework.test.context.TestContext +import org.springframework.test.context.support.AbstractTestExecutionListener +import org.springframework.test.context.support.DependencyInjectionTestExecutionListener + +import static com.lesfurets.jenkins.unit.MethodSignature.method + +class LibraryLoadingTestExecutionListener extends AbstractTestExecutionListener { + + static PipelineTestHelper singletonInstance + + static CompilerConfiguration configuration + + static GroovyClassLoader cLoader + + static { + configuration = new CompilerConfiguration() + cLoader = new InterceptingGCL(singletonInstance, LibraryLoadingTestExecutionListener.class.getClassLoader(), configuration) + } + + static List TRACKED_ON_CLASS = [] + static List TRACKED_ON_METHODS = [] + + static HashMap RESTORE_ON_CLASS = [:] + static HashMap RESTORE_ON_METHODS = [:] + + static boolean START_METHOD_TRACKING = false + static boolean START_CLASS_TRACKING = false + + @Override + int getOrder() { + return 2500 + } + + static void setSingletonInstance(PipelineTestHelper helper) { + if(null != helper) { + helper.metaClass.invokeMethod = { + String name, Object[] args -> + if ((LibraryLoadingTestExecutionListener.START_METHOD_TRACKING || LibraryLoadingTestExecutionListener.START_CLASS_TRACKING) + && name.equals("registerAllowedMethod")) { + + List list + HashMap restore + if (LibraryLoadingTestExecutionListener.START_METHOD_TRACKING) { + list = LibraryLoadingTestExecutionListener.TRACKED_ON_METHODS + restore = LibraryLoadingTestExecutionListener.RESTORE_ON_METHODS + } else if (LibraryLoadingTestExecutionListener.START_CLASS_TRACKING) { + list = LibraryLoadingTestExecutionListener.TRACKED_ON_CLASS + restore = LibraryLoadingTestExecutionListener.RESTORE_ON_CLASS + } + + Object methodName = args[0] + def key + if (args[1] instanceof List) { + List parameters = args[1] + key = method(methodName, parameters.toArray(new Class[parameters.size()])) + } else if (!(args[0] instanceof MethodSignature)) { + key = method(methodName, args[1]) + } + + if (null != key) { + list.add(key) + def existingValue = helper.removeAllowedMethodCallback(key) + if (!restore.containsKey(key)) { + restore.put(key, existingValue) + } + } + } + def m = delegate.metaClass.getMetaMethod(name, *args) + if( null != m) + return m.invoke(delegate, *args) + } + } + singletonInstance = helper + } + + static PipelineTestHelper getSingletonInstance() { + if (singletonInstance == null) { + setSingletonInstance(new LibraryLoadingTestExecutionListener.PipelineTestHelperHook().helper) + } + + return singletonInstance + } + + @Override + void beforeTestClass(TestContext testContext) throws Exception { + super.beforeTestClass(testContext) + def helper = LibraryLoadingTestExecutionListener.getSingletonInstance() + registerDefaultAllowedMethods(helper) + LibraryLoadingTestExecutionListener.START_CLASS_TRACKING = true + } + + @Override + void afterTestClass(TestContext testContext) throws Exception { + super.afterTestClass(testContext) + PipelineTestHelper helper = LibraryLoadingTestExecutionListener.getSingletonInstance() + helper.clearAllowedMethodCallbacks(LibraryLoadingTestExecutionListener.TRACKED_ON_CLASS) + LibraryLoadingTestExecutionListener.TRACKED_ON_CLASS.clear() + + helper.putAllAllowedMethodCallbacks(LibraryLoadingTestExecutionListener.RESTORE_ON_CLASS) + LibraryLoadingTestExecutionListener.RESTORE_ON_CLASS.clear() + + LibraryLoadingTestExecutionListener.START_CLASS_TRACKING = false + + if (Boolean.TRUE.equals(testContext.getAttribute(DependencyInjectionTestExecutionListener.REINJECT_DEPENDENCIES_ATTRIBUTE))) { + LibraryLoadingTestExecutionListener.singletonInstance = null + PipelineTestHelper newHeiper = LibraryLoadingTestExecutionListener.getSingletonInstance() + + def applicationContext = testContext.getApplicationContext() + def beanNames = applicationContext.getBeanDefinitionNames() + beanNames.each { name -> + LibraryLoadingTestExecutionListener.prepareObjectInterceptors(applicationContext.getBean(name)) + } + } + } + + @Override + void beforeTestMethod(TestContext testContext) throws Exception { + super.beforeTestMethod(testContext) + def testInstance = testContext.getTestInstance() + testInstance.binding.setVariable('currentBuild', [result: 'SUCCESS']) + PipelineTestHelper helper = LibraryLoadingTestExecutionListener.getSingletonInstance() + LibraryLoadingTestExecutionListener.START_METHOD_TRACKING = true + } + + @Override + void afterTestMethod(TestContext testContext) throws Exception { + super.afterTestMethod(testContext) + def testInstance = testContext.getTestInstance() + PipelineTestHelper helper = LibraryLoadingTestExecutionListener.getSingletonInstance() + + helper.clearCallStack() + helper.clearAllowedMethodCallbacks(LibraryLoadingTestExecutionListener.TRACKED_ON_METHODS) + LibraryLoadingTestExecutionListener.TRACKED_ON_METHODS.clear() + + LibraryLoadingTestExecutionListener.START_METHOD_TRACKING = false + + testInstance.getNullScript().commonPipelineEnvironment.reset() + } + + def registerDefaultAllowedMethods(helper) { + helper.registerAllowedMethod("stage", [String.class, Closure.class], null) + helper.registerAllowedMethod("stage", [String.class, Closure.class], null) + helper.registerAllowedMethod("node", [String.class, Closure.class], null) + helper.registerAllowedMethod("node", [Closure.class], null) + helper.registerAllowedMethod( method('sh', Map.class), {m -> + println "sh-command: $m.script" + return "" + } ) + helper.registerAllowedMethod( method('sh', String.class), {s -> + println "sh-command: $s" + return "" + } ) + helper.registerAllowedMethod("checkout", [Map.class], null) + helper.registerAllowedMethod("echo", [String.class], null) + helper.registerAllowedMethod("timeout", [Map.class, Closure.class], null) + helper.registerAllowedMethod("step", [Map.class], null) + helper.registerAllowedMethod("input", [String.class], null) + helper.registerAllowedMethod("gitlabCommitStatus", [String.class, Closure.class], { String name, Closure c -> + c.delegate = delegate + helper.callClosure(c) + }) + helper.registerAllowedMethod("gitlabBuilds", [Map.class, Closure.class], null) + helper.registerAllowedMethod("logRotator", [Map.class], null) + helper.registerAllowedMethod("buildDiscarder", [Object.class], null) + helper.registerAllowedMethod("pipelineTriggers", [List.class], null) + helper.registerAllowedMethod("properties", [List.class], null) + helper.registerAllowedMethod("dir", [String.class, Closure.class], null) + helper.registerAllowedMethod("archiveArtifacts", [Map.class], null) + helper.registerAllowedMethod("junit", [String.class], null) + helper.registerAllowedMethod("readFile", [String.class], null) + helper.registerAllowedMethod("disableConcurrentBuilds", [], null) + helper.registerAllowedMethod("gatlingArchive", [], null) + + helper.registerAllowedMethod("unstash", [String.class], null) + helper.registerAllowedMethod("unstash", [Object.class, String.class], null) + helper.registerAllowedMethod("stash", [Map.class], null) + helper.registerAllowedMethod("echo", [String.class], null) + } + + static def prepareObjectInterceptors(Object object) { + object.metaClass.invokeMethod = LibraryLoadingTestExecutionListener.singletonInstance.getMethodInterceptor() + object.metaClass.static.invokeMethod = LibraryLoadingTestExecutionListener.singletonInstance.getMethodInterceptor() + object.metaClass.methodMissing = LibraryLoadingTestExecutionListener.singletonInstance.getMethodMissingInterceptor() + } + + static class PipelineTestHelperHook { + def helper = new PipelineTestHelper() { + def clearAllowedMethodCallbacks(Collection c = []) { + List itemsToRemove = [] + c.each { + key -> + allowedMethodCallbacks.entrySet().each { + entry -> + if (entry?.getKey().equals(key)) + itemsToRemove.add(entry.getKey()) + } + } + allowedMethodCallbacks.keySet().removeAll(itemsToRemove) + } + + def removeAllowedMethodCallback(Object key) { + def itemToRemove + allowedMethodCallbacks.entrySet().each { + entry -> + if (entry?.getKey().equals(key)) { + itemToRemove = entry.getKey() + } + } + if (null != itemToRemove) { + def itemValue = allowedMethodCallbacks.remove(itemToRemove) + return itemValue + } + return null + } + + def putAllAllowedMethodCallbacks(HashMap m) { + m.entrySet().each { + entry -> + if(null != entry.getValue()) + allowedMethodCallbacks.put(entry.getKey(), entry.getValue()) + else + allowedMethodCallbacks.remove(entry.getKey()) + } + } + + protected void setGlobalVars(Binding binding) { + libLoader.libRecords.values().stream() + .flatMap { it.definedGlobalVars.entrySet().stream() } + .forEach { e -> + if (e.value instanceof Script) { + Script script = Script.cast(e.value) + // invoke setBinding from method to avoid interception + SCRIPT_SET_BINDING.invoke(script, binding) + LibraryLoadingTestExecutionListener.prepareObjectInterceptors(script) + script.metaClass.getMethods().findAll { it.name == 'call' }.forEach { m -> + LibraryLoadingTestExecutionListener.singletonInstance.registerAllowedMethod(method(e.value.class.name, m.getNativeParameterTypes()), + { args -> + m.doMethodInvoke(e.value, args) + }) + } + } + binding.setVariable(e.key, e.value) + } + } + } + } +} diff --git a/test/groovy/util/Rules.groovy b/test/groovy/util/Rules.groovy index e9c5791de..6a3b37a96 100644 --- a/test/groovy/util/Rules.groovy +++ b/test/groovy/util/Rules.groovy @@ -13,8 +13,9 @@ public class Rules { public static RuleChain getCommonRules(BasePipelineTest testCase, LibraryConfiguration libConfig) { return RuleChain.outerRule(new JenkinsSetupRule(testCase, libConfig)) - .around(new JenkinsReadYamlRule(testCase)) - .around(new JenkinsResetDefaultCacheRule()) - .around(new JenkinsErrorRule(testCase)) + .around(new JenkinsReadYamlRule(testCase)) + .around(new JenkinsResetDefaultCacheRule()) + .around(new JenkinsErrorRule(testCase)) + .around(new JenkinsEnvironmentRule(testCase)) } } diff --git a/vars/commonPipelineEnvironment.groovy b/vars/commonPipelineEnvironment.groovy index 1ff5bdea6..cb9634b79 100644 --- a/vars/commonPipelineEnvironment.groovy +++ b/vars/commonPipelineEnvironment.groovy @@ -15,12 +15,28 @@ class commonPipelineEnvironment implements Serializable { Map defaultConfiguration = [:] //each Map in influxCustomDataMap represents a measurement in Influx. Additional measurements can be added as a new Map entry of influxCustomDataMap - private Map influxCustomDataMap = [pipeline_data: [:]] + private Map influxCustomDataMap = [pipeline_data: [:], step_data: [:]] //influxCustomData represents measurement jenkins_custom_data in Influx. Metrics can be written into this map private Map influxCustomData = [:] private String mtarFilePath + def reset() { + appContainerProperties = [:] + artifactVersion = null + + configProperties = [:] + configuration = [:] + + gitCommitId = null + gitSshUrl = null + + influxCustomData = [:] + influxCustomDataMap = [pipeline_data: [:], step_data: [:]] + + mtarFilePath = null + } + def setAppContainerProperty(property, value) { appContainerProperties[property] = value } diff --git a/vars/pipelineStashFiles.groovy b/vars/pipelineStashFiles.groovy new file mode 100644 index 000000000..eb2ea5214 --- /dev/null +++ b/vars/pipelineStashFiles.groovy @@ -0,0 +1,7 @@ +def call(Map parameters = [:], body) { + handlePipelineStepErrors (stepName: 'pipelineStashFiles', stepParameters: parameters) { + pipelineStashFilesBeforeBuild(parameters) + body() //execute build + pipelineStashFilesAfterBuild(parameters) + } +} diff --git a/vars/pipelineStashFilesAfterBuild.groovy b/vars/pipelineStashFilesAfterBuild.groovy new file mode 100644 index 000000000..96649f3ef --- /dev/null +++ b/vars/pipelineStashFilesAfterBuild.groovy @@ -0,0 +1,51 @@ +import com.sap.piper.Utils +import com.sap.piper.ConfigurationHelper +import groovy.transform.Field + +@Field String STEP_NAME = 'pipelineStashFilesAfterBuild' +@Field Set STEP_CONFIG_KEYS = ['runCheckmarx', 'stashIncludes', 'stashExcludes'] +@Field Set PARAMETER_KEYS = STEP_CONFIG_KEYS + +def call(Map parameters = [:]) { + + handlePipelineStepErrors (stepName: STEP_NAME, stepParameters: parameters, stepNameDoc: 'stashFiles') { + def utils = parameters.juStabUtils + if (utils == null) { + utils = new Utils() + } + def script = parameters.script + if (script == null) + script = [commonPipelineEnvironment: commonPipelineEnvironment] + + //additional includes via passing e.g. stashIncludes: [opa5: '**/*.include'] + //additional excludes via passing e.g. stashExcludes: [opa5: '**/*.exclude'] + + Map config = ConfigurationHelper + .loadStepDefaults(this) + .mixinGeneralConfig(script.commonPipelineEnvironment, STEP_CONFIG_KEYS) + .mixinStageConfig(script.commonPipelineEnvironment, parameters.stageName?:env.STAGE_NAME, STEP_CONFIG_KEYS) + .mixinStepConfig(script.commonPipelineEnvironment, STEP_CONFIG_KEYS) + .mixin(parameters, PARAMETER_KEYS) + .addIfEmpty('runCheckmarx', (script.commonPipelineEnvironment.configuration?.steps?.executeCheckmarxScan?.checkmarxProject != null && script.commonPipelineEnvironment.configuration.steps.executeCheckmarxScan.checkmarxProject.length()>0)) + .use() + + // store files to be checked with checkmarx + if (config.runCheckmarx) { + utils.stash('checkmarx', config.stashIncludes?.get('checkmarx')?config.stashIncludes.checkmarx:'**/*.js, **/*.scala, **/*.py, **/*.go, **/*.xml, **/*.html', config.stashExcludes?.get('checkmarx')?config.stashExcludes.checkmarx:'**/*.mockserver.js, node_modules/**/*.js') + } + + utils.stashWithMessage( + 'classFiles', + '[${STEP_NAME}] Failed to stash class files.', + config.stashIncludes.classFiles, + config.stashExcludes.classFiles + ) + + utils.stashWithMessage( + 'sonar', + '[${STEP_NAME}] Failed to stash sonar files.', + config.stashIncludes.sonar, + config.stashExcludes.sonar + ) + } +} diff --git a/vars/pipelineStashFilesBeforeBuild.groovy b/vars/pipelineStashFilesBeforeBuild.groovy new file mode 100644 index 000000000..a1569b801 --- /dev/null +++ b/vars/pipelineStashFilesBeforeBuild.groovy @@ -0,0 +1,97 @@ +import com.sap.piper.Utils +import com.sap.piper.ConfigurationHelper +import groovy.transform.Field + +@Field String STEP_NAME = 'pipelineStashFilesBeforeBuild' +@Field Set STEP_CONFIG_KEYS = ['runOpaTests', 'stashIncludes', 'stashExcludes'] +@Field Set PARAMETER_KEYS = STEP_CONFIG_KEYS + +def call(Map parameters = [:]) { + + handlePipelineStepErrors (stepName: STEP_NAME, stepParameters: parameters, stepNameDoc: 'stashFiles') { + + def utils = parameters.juStabUtils + if (utils == null) { + utils = new Utils() + } + + def script = parameters.script + if (script == null) + script = [commonPipelineEnvironment: commonPipelineEnvironment] + + //additional includes via passing e.g. stashIncludes: [opa5: '**/*.include'] + //additional excludes via passing e.g. stashExcludes: [opa5: '**/*.exclude'] + + Map config = ConfigurationHelper + .loadStepDefaults(this) + .mixinGeneralConfig(script.commonPipelineEnvironment, STEP_CONFIG_KEYS) + .mixinStageConfig(script.commonPipelineEnvironment, parameters.stageName?:env.STAGE_NAME, STEP_CONFIG_KEYS) + .mixinStepConfig(script.commonPipelineEnvironment, STEP_CONFIG_KEYS) + .mixin(parameters, PARAMETER_KEYS) + .use() + + if (config.runOpaTests){ + utils.stash('opa5', config.stashIncludes?.get('opa5')?config.stashIncludes.opa5:'**/*.*', config.stashExcludes?.get('opa5')?config.stashExcludes.opa5:'') + } + + //store git metadata for SourceClear agent + sh "mkdir -p gitmetadata" + sh "cp -rf .git/* gitmetadata" + sh "chmod -R u+w gitmetadata" + utils.stashWithMessage( + 'git', + '[${STEP_NAME}] no git repo files detected: ', + config.stashIncludes.git, + config.stashExcludes.git + ) + + //store files required for tests, e.g. Gauge, SUT, ... + utils.stashWithMessage( + 'tests', + '[${STEP_NAME}] no files for tests provided: ', + config.stashIncludes.tests, + config.stashExcludes.tests + ) + //store build descriptor files depending on technology, e.g. pom.xml, package.json + utils.stash( + 'buildDescriptor', + config.stashIncludes.buildDescriptor, + config.stashExcludes.buildDescriptor + ) + //store deployment descriptor files depending on technology, e.g. *.mtaext.yml + utils.stashWithMessage( + 'deployDescriptor', + '[${STEP_NAME}] no deployment descriptor files provided: ', + config.stashIncludes.deployDescriptor, + config.stashExcludes.deployDescriptor + ) + //store nsp & retire exclusion file for future use + utils.stashWithMessage( + 'opensource configuration', + '[${STEP_NAME}] no opensource configuration files provided: ', + config.stashIncludes.get('opensource configuration'), + config.stashExcludes.get('opensource configuration') + ) + //store snyk config file for future use + utils.stashWithMessage( + 'snyk configuration', + '[${STEP_NAME}] no snyk configuration files provided: ', + config.stashIncludes.get('snyk configuration'), + config.stashExcludes.get('snyk configuration') + ) + //store pipeline configuration including additional groovy test scripts for future use + utils.stashWithMessage( + 'pipelineConfigAndTests', + '[${STEP_NAME}] no pipeline configuration and test files found: ', + config.stashIncludes.pipelineConfigAndTests, + config.stashExcludes.pipelineConfigAndTests + ) + + utils.stashWithMessage( + 'securityDescriptor', + '[${STEP_NAME}] no security descriptor found: ', + config.stashIncludes.securityDescriptor, + config.stashExcludes.securityDescriptor + ) + } +}