Browse Source

pipeline: Test more of the stage steps implementation

Functionally testing pipeline stage steps (build, run, publish, deploy,
exports) is tricky on account of either having to mock all calls to
workflow script and/or pipeline runner methods, or somehow setup
dependent backend systems such as k8s for systems tests.

The lack of coverage for these methods has resulted in a number of found
bugs and incremental fixes that could have been found through testing,
so the value of heavy mocking and system-test complexity in these cases
seems to outweigh the cost.

By installing a stubbed version of the `docker-pusher` script into the
Jenkins container, we can run a system test for the `publish` step, and
by stubbing out some of the previous stage/step context, we can more
effectively test `PipelineStage` step methods.

Change-Id: Ibed4f781e6c46dcbf5741eaf8adb79ebe4ef2d39
master
Dan Duvall 2 years ago
parent
commit
ed9a1ab248
7 changed files with 305 additions and 11 deletions
  1. +4
    -3
      src/org/wikimedia/integration/PipelineStage.groovy
  2. +4
    -0
      systemtests/jenkins/Dockerfile
  3. +3
    -0
      systemtests/jenkins/bin/stub-script
  4. +1
    -1
      systemtests/jenkins/init.groovy.d/02-nodes.groovy
  5. +1
    -0
      systemtests/jenkins/sudoers
  6. +4
    -0
      systemtests/repo1/.pipeline/config.yaml
  7. +288
    -7
      test/org/wikimedia/integration/PipelineStageTest.groovy

+ 4
- 3
src/org/wikimedia/integration/PipelineStage.groovy View File

@ -171,7 +171,7 @@ class PipelineStage implements Serializable {
ws.dir(pipeline.directory) {
for (def stageStep in STEPS) {
if (config[stageStep]) {
ws.echo("step: ${stageStep}")
ws.echo("step: ${stageStep}, config: ${config.inspect()}")
this."${stageStep}"(ws, runner)
}
}
@ -526,8 +526,9 @@ class PipelineStage implements Serializable {
* </dl>
*/
void exports(ws, runner) {
config.exports.each { name, value ->
context[name] = context % value
config.exports.each { export, value ->
context[export] = context % value
ws.echo "exported ${name}.${export}=${context[export].inspect()}"
}
}
}

+ 4
- 0
systemtests/jenkins/Dockerfile View File

@ -15,6 +15,7 @@ RUN apt-get update && \
ca-certificates \
curl \
gnupg2 \
sudo \
software-properties-common && \
( curl -fsSL https://download.docker.com/linux/debian/gpg | apt-key add - ) && \
add-apt-repository \
@ -23,6 +24,9 @@ RUN apt-get update && \
apt-get install -y docker-ce-cli && \
rm -rf /var/lib/apt/lists/*
COPY systemtests/jenkins/bin/stub-script /usr/local/bin/docker-pusher
COPY systemtests/jenkins/sudoers /etc/sudoers.d/jenkins
USER jenkins
RUN echo $VERSION > /usr/share/jenkins/ref/jenkins.install.UpgradeWizard.state


+ 3
- 0
systemtests/jenkins/bin/stub-script View File

@ -0,0 +1,3 @@
#!/bin/sh
echo "stubbed execution of: ($0 $@)"

+ 1
- 1
systemtests/jenkins/init.groovy.d/02-nodes.groovy View File

@ -1,7 +1,7 @@
import jenkins.model.Jenkins
Jenkins.instance.computers.each { c ->
c.node.labelString += "blubber"
c.node.labelString += "blubber dockerPublish"
}
Jenkins.instance.save()

+ 1
- 0
systemtests/jenkins/sudoers View File

@ -0,0 +1 @@
jenkins ALL=(root) NOPASSWD: /usr/local/bin/docker-pusher

+ 4
- 0
systemtests/repo1/.pipeline/config.yaml View File

@ -9,6 +9,8 @@ pipelines:
- name: candidate
build: production
run: false
publish:
image: true
execution:
- [unit, candidate]
- [lint, candidate]
@ -16,3 +18,5 @@ pipelines:
directory: src/service2
stages:
- name: test
exports:
foo: '${setup.timestamp}-${.stage}'

+ 288
- 7
test/org/wikimedia/integration/PipelineStageTest.groovy View File

@ -111,31 +111,312 @@ class PipelineStageTest extends GroovyTestCase {
]
}
void testExports() {
void testBuild() {
def pipeline = new Pipeline("foo", [
stages: [
[
name: "foo",
build: "foovariant",
],
]
])
def mockRunner = new MockFor(PipelineRunner)
def stack = pipeline.stack()
def stage = stack[1][0]
stubStageContexts(stack, [
setup: { ctx ->
ctx["imageLabels"] = [foo: "foolabel", bar: "barlabel"]
},
])
mockRunner.demand.build { variant, labels ->
assert variant == "foovariant"
assert labels == [foo: "foolabel", bar: "barlabel"]
"foo-image-id"
}
mockRunner.use {
def ws = new WorkflowScript()
def runner = new PipelineRunner(ws)
stage.build(ws, runner)
}
assert stage.context["imageID"] == "foo-image-id"
}
void testDeploy_withTest() {
def pipeline = new Pipeline("foo", [
stages: [
[
name: "foo",
build: "foovariant",
publish: [
image: true,
],
],
[
name: "bar",
deploy: [
image: '${foo.publishedImage}',
chart: 'https://an.example/chart/foo-${.stage}.tar.gz',
tag: '${.stage}-tag',
test: true,
],
],
]
])
def mockRunner = new MockFor(PipelineRunner)
def stack = pipeline.stack()
def stage = stack[2][0]
stubStageContexts(stack, [
foo: { ctx ->
ctx["imageID"] = "foo-image-id"
ctx["imageName"] = "foo-project"
ctx["imageFullName"] = "registry.example/bar/foo-project"
ctx["imageTag"] = "0000-00-00-000000-foo"
ctx["imageTags"] = ["0000-00-00-000000-foo"]
ctx["publishedImage"] = "registry.example/bar/foo-project:0000-00-00-000000-foo"
},
])
mockRunner.demand.deployWithChart { chart, image, tag ->
assert chart == "https://an.example/chart/foo-bar.tar.gz"
assert image == "registry.example/bar/foo-project:0000-00-00-000000-foo"
assert tag == "bar-tag"
"bar-release-name"
}
mockRunner.demand.testRelease { releaseName ->
assert releaseName == "bar-release-name"
}
mockRunner.use {
def ws = new WorkflowScript()
def runner = new PipelineRunner(ws)
stage.deploy(ws, runner)
}
assert stage.context["releaseName"] == "bar-release-name"
}
void testDeploy_withoutTest() {
def pipeline = new Pipeline("foo", [
stages: [
[
name: "foo",
build: "foovariant",
publish: [
image: true,
],
],
[
name: "bar",
deploy: [
image: '${foo.publishedImage}',
chart: 'https://an.example/chart/foo-${.stage}.tar.gz',
tag: '${.stage}-tag',
test: false,
],
],
]
])
def mockRunner = new MockFor(PipelineRunner)
def stack = pipeline.stack()
def stage = stack[2][0]
stubStageContexts(stack, [
foo: { ctx ->
ctx["imageID"] = "foo-image-id"
ctx["imageName"] = "foo-project"
ctx["imageFullName"] = "registry.example/bar/foo-project"
ctx["imageTag"] = "0000-00-00-000000-foo"
ctx["imageTags"] = ["0000-00-00-000000-foo"]
ctx["publishedImage"] = "registry.example/bar/foo-project:0000-00-00-000000-foo"
},
])
mockRunner.demand.deployWithChart { chart, image, tag ->
assert chart == "https://an.example/chart/foo-bar.tar.gz"
assert image == "registry.example/bar/foo-project:0000-00-00-000000-foo"
assert tag == "bar-tag"
"bar-release-name"
}
mockRunner.use {
def ws = new WorkflowScript()
def runner = new PipelineRunner(ws)
stage.deploy(ws, runner)
}
assert stage.context["releaseName"] == "bar-release-name"
}
void testExports() {
def pipeline = new Pipeline("foo", [
stages: [
[
name: "foo",
build: "foovariant",
exports: [
image: "fooimage",
thing: '${.imageID}-${setup.timestamp}',
],
]
],
]
])
def mockRunner = new MockFor(PipelineRunner)
def mockWS = new MockFor(WorkflowScript)
def stack = pipeline.stack()
def stage = stack[1][0]
def stage = pipeline.stack()[1][0]
stubStageContexts(stack, [
setup: { ctx ->
ctx["timestamp"] = "0000-00-00-000000"
},
foo: { ctx ->
ctx["imageID"] = "foo-image-id"
},
])
assert stage.name == "bar"
mockWS.demand.echo { msg ->
assert msg == "exported foo.thing='foo-image-id-0000-00-00-000000'"
}
mockWS.use {
mockRunner.use {
stage.exports(mockWS, mockRunner)
def ws = new WorkflowScript()
def runner = new PipelineRunner(ws)
stage.exports(ws, runner)
}
}
assert stage.context["image"] == "fooimage"
assert stage.context["thing"] == "foo-image-id-0000-00-00-000000"
}
void testPublish() {
def pipeline = new Pipeline("foopipeline", [
stages: [
[
name: "built",
build: "foovariant",
],
[
name: "published",
publish: [
image: [
id: '${built.imageID}',
],
],
]
]
])
def mockRunner = new MockFor(PipelineRunner)
def stack = pipeline.stack()
def stage = stack[2][0]
stubStageContexts(stack, [
setup: { ctx ->
ctx["project"] = "foo-project"
ctx["timestamp"] = "0000-00-00-000000"
ctx["imageLabels"] = []
},
built: { ctx ->
ctx["imageID"] = "foo-image-id"
},
])
mockRunner.demand.registerAs { imageID, imageName, tag ->
assert imageID == "foo-image-id"
assert imageName == "foo-project"
assert tag == "0000-00-00-000000-published"
}
mockRunner.demand.qualifyRegistryPath { imageName ->
assert imageName == "foo-project"
"registry.example/bar/foo-project"
}
mockRunner.use {
def ws = new WorkflowScript()
def runner = new PipelineRunner(ws)
stage.publish(ws, runner)
}
assert stage.context["imageName"] == "foo-project"
assert stage.context["imageFullName"] == "registry.example/bar/foo-project"
assert stage.context["imageTag"] == "0000-00-00-000000-published"
assert stage.context["imageTags"] == ["0000-00-00-000000-published"]
assert stage.context["publishedImage"] == "registry.example/bar/foo-project:0000-00-00-000000-published"
}
void testRun() {
def pipeline = new Pipeline("foo", [
stages: [
[
name: "foo",
build: "foovariant",
],
[
name: "bar",
run: [
image: '${foo.imageID}',
arguments: ['${.stage}arg'],
],
],
]
])
def mockRunner = new MockFor(PipelineRunner)
def stack = pipeline.stack()
def stage = stack[2][0]
stubStageContexts(stack, [
foo: { ctx ->
ctx["imageID"] = "foo-image-id"
},
])
mockRunner.demand.run { image, args ->
assert image == "foo-image-id"
assert args == ["bararg"]
}
mockRunner.use {
def ws = new WorkflowScript()
def runner = new PipelineRunner(ws)
stage.run(ws, runner)
}
}
private void stubStageContexts(stack, stubs) {
stack.each { stages ->
stages.each { stage ->
stage.context["stage"] = stage.name
stubs.each { stageName, stub ->
if (stage.name == stageName) {
stub(stage.context)
}
}
}
}
}
}

Loading…
Cancel
Save