diff --git a/documentation/docs/steps/batsExecuteTests.md b/documentation/docs/steps/batsExecuteTests.md new file mode 100644 index 000000000..8b4311831 --- /dev/null +++ b/documentation/docs/steps/batsExecuteTests.md @@ -0,0 +1,77 @@ +# batsExecuteTests + +## Description + +This step executes tests using the [Bash Automated Testing System - bats-core](https://github.com/bats-core/bats-core) + +## Prerequsites + +You need to have a Bats test file. By default you would put this into directory `src/test` within your source code repository. + +## Parameters + +| parameter | mandatory | default | possible values | +|-----------|-----------|---------|-----------------| +| script | no | empty `globalPipelineEnvironment` | | +| dockerImage | no | `node:8-stretch` | | +| dockerWorkspace | no |`/home/node`| | +| envVars | no | `[:]` | | +| failOnError | no | `false` | | +| gitBranch | no | | | +| gitSshKeyCredentialsId | no | | | +| outputFormat | no | `junit` | `tap` | +| repository | no | `https://github.com/bats-core/bats-core.git` | | +| stashContent | no | `['tests']` | | +| testPackage | no | `piper-bats` | | +| testPath | no | `src/test`| | +| testRepository | no | | | + + +Details: + +* `outputFormat` defines the format of the test result output. `junit` would be the standard for automated build environments but you could use also the option `tap`. +* For the transformation of the test result to xUnit format the node module **tap-xunit** is used. `dockerImage` and `dockerWorkspace` define the Docker image used for the transformation and `testPackage` defines the name of the test package used in the xUnit result file. +* `testPath` defines either the directory which contains the test files (`*.bats`) or a single file. You can find further details in the [Bats-core documentation](https://github.com/bats-core/bats-core#usage) +* With `failOnError` you can define the behavior, in case tests fail. For example, in case of `outputFormat: 'junit'` you should set it to `false`. Otherwise test results cannot be recorded using the `testsPublishhResults` step afterwards. +* You can pass environment variables to the test execution by defining parameter `envVars`. + + With `envVars` it is possible to pass either fixed values but also templates using [`commonPipelineEnvironment`](commonPipelineEnvironment.md). + + Example: + ``` + batsExecuteTests script: this, envVars = [ + FIX_VALUE: 'my fixed value', + CONTAINER_NAME: '${commonPipelineEnvironment.configuration.steps.executeBatsTests.dockerContainerName}', + IMAGE_NAME: '${return commonPipelineEnvironment.getDockerImageNameAndTag()}' + ] + ``` + + This means within the test one could refer to environment variables by calling e.g. + `run docker run --rm -i --name $CONTAINER_NAME --entrypoint /bin/bash $IMAGE_NAME echo "Test"` + +* Using parameters `testRepository` the tests can be loaded from another reposirory. In case the tests are not located in the master branch the branch can be specified with `gitBranch`. For protected repositories you can also define the access credentials via `gitSshKeyCredentialsId`. **Note: In case of using a protected repository, `testRepository` should include the ssh link to the repository.** +* The parameter `repository` defines the version of **bats-core** to be used. By default we use the version from the master branch. + +## Step configuration +The following parameters can also be specified as step/stage/general parameters using the [global configuration](../configuration.md): + +* dockerImage +* dockerWorkspace +* envVars +* failOnError +* gitBranch +* gitSshKeyCredentialsId +* outputFormat +* repository +* stashContent +* testPackage +* testPath +* testRepository + + +## Example + +```groovy +batsExecuteTests script:this +testsPublishResults junit: [pattern: '**/Test-*.xml', archive: true] +``` diff --git a/documentation/mkdocs.yml b/documentation/mkdocs.yml index cbfa3d03d..3cd2f45ed 100644 --- a/documentation/mkdocs.yml +++ b/documentation/mkdocs.yml @@ -4,6 +4,7 @@ pages: - Configuration: configuration.md - 'Library steps': - artifactSetVersion: steps/artifactSetVersion.md + - batsExecuteTests: steps/batsExecuteTests.md - checkChangeInDevelopment: steps/checkChangeInDevelopment.md - cloudFoundryDeploy: steps/cloudFoundryDeploy.md - commonPipelineEnvironment: steps/commonPipelineEnvironment.md diff --git a/resources/default_pipeline_environment.yml b/resources/default_pipeline_environment.yml index b5d141bb1..3070a3cde 100644 --- a/resources/default_pipeline_environment.yml +++ b/resources/default_pipeline_environment.yml @@ -42,6 +42,17 @@ steps: sbt: filePath: 'sbtDescriptor.json' versioningTemplate: '${version}-${timestamp}${commitId?"+"+commitId:""}' + batsExecuteTests: + dockerImage: 'node:8-stretch' + dockerWorkspace: '/home/node' + envVars: {} + outputFormat: 'junit' # tap, junit + testPath: 'src/test' + failOnError: false + repository: 'https://github.com/bats-core/bats-core.git' + stashContent: + - 'tests' + testPackage: 'piper-bats' checksPublishResults: aggregation: active: true diff --git a/test/groovy/BatsExecuteTestsTest.groovy b/test/groovy/BatsExecuteTestsTest.groovy new file mode 100644 index 000000000..004475757 --- /dev/null +++ b/test/groovy/BatsExecuteTestsTest.groovy @@ -0,0 +1,143 @@ +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.rules.ExpectedException +import org.junit.rules.RuleChain +import util.* + +import static org.hamcrest.Matchers.hasItem +import static org.hamcrest.Matchers.is +import static org.junit.Assert.assertThat + +class BatsExecuteTestsTest extends BasePiperTest { + + private ExpectedException thrown = ExpectedException.none() + private JenkinsStepRule jsr = new JenkinsStepRule(this) + private JenkinsLoggingRule jlr = new JenkinsLoggingRule(this) + private JenkinsShellCallRule jscr = new JenkinsShellCallRule(this) + private JenkinsDockerExecuteRule jder = new JenkinsDockerExecuteRule(this) + + @Rule + public RuleChain rules = Rules + .getCommonRules(this) + .around(thrown) + .around(jder) + .around(jscr) + .around(jlr) + .around(jsr) + + List withEnvArgs = [] + + @Before + void init() throws Exception { + helper.registerAllowedMethod("withEnv", [List.class, Closure.class], {arguments, closure -> + arguments.each {arg -> + withEnvArgs.add(arg.toString()) + } + return closure() + }) + } + + @Test + void testDefault() { + nullScript.commonPipelineEnvironment.configuration = [general: [container: 'test-container']] + jsr.step.batsExecuteTests( + script: nullScript, + juStabUtils: utils, + dockerContainerName: 'test-container', + dockerImageNameAndTag: 'test/image', + envVars: [ + IMAGE_NAME: 'test/image', + CONTAINER_NAME: '${commonPipelineEnvironment.configuration.general.container}' + ], + testPackage: 'testPackage' + ) + // asserts + assertThat(withEnvArgs, hasItem('IMAGE_NAME=test/image')) + assertThat(withEnvArgs, hasItem('CONTAINER_NAME=test-container')) + assertThat(jscr.shell, hasItem('git clone https://github.com/bats-core/bats-core.git')) + assertThat(jscr.shell, hasItem('bats-core/bin/bats --recursive --tap src/test > \'TEST-testPackage.tap\'')) + assertThat(jscr.shell, hasItem('cat \'TEST-testPackage.tap\'')) + + assertThat(jder.dockerParams.dockerImage, is('node:8-stretch')) + assertThat(jder.dockerParams.dockerWorkspace, is('/home/node')) + + assertThat(jscr.shell, hasItem('npm install tap-xunit -g')) + assertThat(jscr.shell, hasItem('cat \'TEST-testPackage.tap\' | tap-xunit --package=\'testPackage\' > TEST-testPackage.xml')) + + assertJobStatusSuccess() + } + + @Test + void testTap() { + jsr.step.batsExecuteTests( + script: nullScript, + juStabUtils: utils, + outputFormat: 'tap' + ) + assertThat(jder.dockerParams.size(), is(0)) + } + + @Test + void testFailOnError() { + helper.registerAllowedMethod('sh', [String.class], {s -> + if (s.startsWith('bats-core/bin/bats')) { + throw new Exception('Shell call failed') + } else { + return null + } + }) + thrown.expectMessage('Shell call failed') + jsr.step.batsExecuteTests( + script: nullScript, + juStabUtils: utils, + failOnError: true, + ) + + } + + @Test + void testGit() { + def gitRepository + helper.registerAllowedMethod('git', [Map.class], {m -> + gitRepository = m + }) + helper.registerAllowedMethod('stash', [String.class], {s -> + assertThat(s, is('batsTests')) + }) + + jsr.step.batsExecuteTests( + script: nullScript, + juStabUtils: utils, + testRepository: 'testRepo', + ) + + assertThat(gitRepository.size(), is(1)) + assertThat(gitRepository.url, is('testRepo')) + assertThat(jder.dockerParams.stashContent, hasItem('batsTests')) + } + + @Test + void testGitBranchAndCredentials() { + def gitRepository + helper.registerAllowedMethod('git', [Map.class], {m -> + gitRepository = m + }) + helper.registerAllowedMethod('stash', [String.class], {s -> + assertThat(s, is('batsTests')) + }) + + jsr.step.batsExecuteTests( + script: nullScript, + juStabUtils: utils, + gitBranch: 'test', + gitSshKeyCredentialsId: 'testCredentials', + testRepository: 'testRepo', + ) + assertThat(gitRepository.size(), is(3)) + assertThat(gitRepository.credentialsId, is('testCredentials')) + assertThat(gitRepository.branch, is('test')) + } + + +} diff --git a/vars/batsExecuteTests.groovy b/vars/batsExecuteTests.groovy new file mode 100644 index 000000000..c9bf894bd --- /dev/null +++ b/vars/batsExecuteTests.groovy @@ -0,0 +1,80 @@ +import com.sap.piper.Utils +import com.sap.piper.ConfigurationHelper +import groovy.text.SimpleTemplateEngine +import groovy.transform.Field + +@Field String STEP_NAME = 'batsExecuteTests' +@Field Set STEP_CONFIG_KEYS = [ + 'dockerImage', // + 'dockerWorkspace', + 'envVars', + 'failOnError', + 'gitBranch', + 'gitSshKeyCredentialsId', + 'outputFormat', + 'repository', + 'stashContent', + 'testPackage', + 'testPath', + 'testRepository' +] +@Field Set PARAMETER_KEYS = STEP_CONFIG_KEYS + +def call(Map parameters = [:]) { + handlePipelineStepErrors (stepName: STEP_NAME, stepParameters: parameters) { + + def utils = parameters.juStabUtils ?: new Utils() + def script = parameters.script ?: [commonPipelineEnvironment: commonPipelineEnvironment] + + Map config = ConfigurationHelper + .loadStepDefaults(this) + .mixinGeneralConfig(script.commonPipelineEnvironment, STEP_CONFIG_KEYS) + .mixinStepConfig(script.commonPipelineEnvironment, STEP_CONFIG_KEYS) + .mixinStageConfig(script.commonPipelineEnvironment, parameters.stageName?:env.STAGE_NAME, STEP_CONFIG_KEYS) + .mixin(parameters, PARAMETER_KEYS) + .use() + + // report to SWA + utils.pushToSWA([step: STEP_NAME], config) + + script.commonPipelineEnvironment.setInfluxStepData('bats', false) + + + if (config.testRepository) { + def gitParameters = [url: config.testRepository] + if (config.gitSshKeyCredentialsId?.length()>0) gitParameters.credentialsId = config.gitSshKeyCredentialsId + if (config.gitBranch?.length()>0) gitParameters.branch = config.gitBranch + git gitParameters + stash 'batsTests' + config.stashContent = ['batsTests'] + } else { + config.stashContent = utils.unstashAll(config.stashContent) + } + + //resolve commonPipelineEnvironment references in envVars + config.envVarList = [] + config.envVars.each {e -> + def envValue = SimpleTemplateEngine.newInstance().createTemplate(e.getValue()).make(commonPipelineEnvironment: script.commonPipelineEnvironment).toString() + config.envVarList.add("${e.getKey()}=${envValue}") + } + + withEnv(config.envVarList) { + sh "git clone ${config.repository}" + try { + sh "bats-core/bin/bats --recursive --tap ${config.testPath} > 'TEST-${config.testPackage}.tap'" + script.commonPipelineEnvironment.setInfluxStepData('bats', true) + } catch (err) { + echo "[${STEP_NAME}] One or more tests failed" + if (config.failOnError) throw err + } finally { + sh "cat 'TEST-${config.testPackage}.tap'" + if (config.outputFormat == 'junit') { + dockerExecute(dockerImage: config.dockerImage, dockerWorkspace: config.dockerWorkspace, stashContent: config.stashContent) { + sh "npm install tap-xunit -g" + sh "cat 'TEST-${config.testPackage}.tap' | tap-xunit --package='${config.testPackage}' > TEST-${config.testPackage}.xml" + } + } + } + } + } +} diff --git a/vars/commonPipelineEnvironment.groovy b/vars/commonPipelineEnvironment.groovy index cb9634b79..eade1b078 100644 --- a/vars/commonPipelineEnvironment.groovy +++ b/vars/commonPipelineEnvironment.groovy @@ -96,6 +96,14 @@ class commonPipelineEnvironment implements Serializable { return influxCustomDataMap } + def setInfluxStepData (dataKey, value) { + influxCustomDataMap.step_data[dataKey] = value + } + def getInfluxStepData (dataKey) { + return influxCustomDataMap.step_data[dataKey] + } + + def getMtarFilePath() { return mtarFilePath }