jenkinsci / JenkinsPipelineUnit

Framework for unit testing Jenkins pipelines
MIT License
1.55k stars 394 forks source link

Params not accessible in SharedLibrary classes #478

Open MichaelJKar opened 2 years ago

MichaelJKar commented 2 years ago

Jenkins and plugins versions report

Environment ```text See minimal viable example below. Plugins should not play a role here. ```

What Operating System are you using (both controller, and any agents involved in the problem)?

Windows

Reproduction steps

Jenkinsfile:

#!groovy
@Library('lib')
import LibClass

pipeline {
    stages {
        stage('Test stage'){
            steps {
                out = new LibClass().getX()
                echo "Printing ${out}"
            }
        }
    }
}

Test Class:

import com.lesfurets.jenkins.unit.declarative.DeclarativePipelineTest
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test

import static com.lesfurets.jenkins.unit.global.lib.LibraryConfiguration.library
import static com.lesfurets.jenkins.unit.global.lib.ProjectSource.projectSource

class MainPipelineIntegrationTest extends DeclarativePipelineTest {

    @Override
    @BeforeEach
    void setUp() throws Exception {
        super.setUp()
        def library = library().name("lib")
                .retriever(projectSource("./mocklib"))
                .defaultVersion("master")
                .targetPath("target/mocklib")
                .allowOverride(true)
                .implicit(false)
                .build()
        helper.registerSharedLibrary(library)
    }

    @Test
    void test() {
        binding.setVariable('env', ['X':'testXValue'])
        binding.setVariable('params', ['X':'testXValue'])

        runScript("../Jenkinsfile")

        assertJobStatusSuccess()
        printCallStack()
    }
}

The shared library lib only contains one file LibClass.groovy with the following content:

class LibClass {
    def getX(){
        return params.X
    }
}

Expected Results

Loading shared library lib with version master
Printing testXValue
   Jenkinsfile.run()
      Jenkinsfile.pipeline(groovy.lang.Closure)
         Jenkinsfile.stage(Test stage, groovy.lang.Closure)
            LibClass.getX()
            Jenkinsfile.echo(Printing testXValue)

Actual Results

Loading shared library lib with version master

groovy.lang.MissingPropertyException: No such property: params for class: LibClass

Anything else?

When replacing the content of getX() with return env.X, the test runs successfully and the output matches the expected output given above.

I don't know why this differentiation between env and params exists in the testing framework, since these two maps are essentially equivalent concepts in Jenkins itself. If the env map is available in a class within the shared library, then params should also be available.

Note: I already commented on this problem in #402, but since the problem is not 100% the same, I opened a new ticket.

elora-walmisley commented 2 years ago

We're hitting a similar issue that we believe has the same root cause.

We're are trying to write a test that includes the pullRequest global variable form the pipeline-github-plugin. We are mocking it out with the following call

binding.setVariable('pullRequest', new PullRequestMock())

This works as expected when executing the following test

pipeline {
  stages {
    stage('test pullRequest global variable') {
      steps {
        script {
          pullRequest.comment("foobar")
        }
      }
    }
  }
}

And also works as expected from the first level of our library

pipeline {
  stages {
    stage('test pullRequest global variable') {
      steps {
        script {
          jenkinslibrary.commentOnPullRequest("foobar")
        }
      }
    }
  }
}

Where there exists a vars/jenkinslibrary.groovy file with the following method

def commentOnPullRequest(String comment) {
    pullRequest.comment(comment)
}

However, if we try to access that global variable inside another groovy class - for example src/pullRequestUtils.groovy

def commentOnPullRequest(String comment) {
     def prUtils = new com.jenkinslibrary.pullRequestUtils()
     prUtils.comment(comment)
}
def comment(String comment) {
     pullRequest.comment(comment)
}

The execution fails with the following error

groovy.lang.MissingPropertyException: No such property: pullRequest for class: com.jenkinslibrary.pullRequestUtils
    at app//groovy.lang.MetaClassImpl.invokeStaticMissingProperty(MetaClassImpl.java:1019)
    at app//groovy.lang.MetaClassImpl.setProperty(MetaClassImpl.java:2862)
    at app//groovy.lang.MetaClassImpl.setProperty(MetaClassImpl.java:3854)

It looks like the global variable is not available inside the groovy classes, similar to params above. Mocking methods from plugins works as expected, just not global variables.

This code runs fine in our actual jenkins pipelines, i.e. we can access the global pullRequest variable from inside the class.

Workaround

We are currently working around issue by adding a simple method that returns the pullRequest object inside our class, and mocking that as part of the test

def pullRequestDetails() {
    return pullRequest
}

and then in the test file

helper.registerAllowedMethod('pullRequestDetails', [], { return new PullRequestMock() })

Which allows us to access the object

nre-ableton commented 2 years ago

@MichaelJKar does the above workaround work for you? Can this issue be closed?

elora-walmisley commented 2 years ago

@nre-ableton We'd prefer not to use the workaround forever if possible. It's okay for this situation, but it requires you to set the same mock in two places if you want to use the pullRequest object (or the params object in @MichaelJKar's case) in both the pipeline and the library class. It also requires that you to add extra methods to your library classes to facilitate any testing that involves parameters or global plugin objects, which isn't ideal.

svyotov commented 2 years ago

Indeed, the work around is not a good solution long term, and could cause issues as it forces changes in actual code just to make the tests work. It is also something a developer would waste time debugging if they do not know it is an issue of the testing framework.

nre-ableton commented 2 years ago

On closer inspection, what if you pass the script context to the library? That is:

Jenkinsfile:

#!groovy
@Library('lib')
import LibClass

pipeline {
    stages {
        stage('Test stage') {
            steps {
                out = new LibClass(script: this).getX()
                echo "Printing ${out}"
            }
        }
    }
}

LibClass.groovy:

class LibClass {
    Object script

    def getX() {
        return script.params.X
    }
}
nestoracunablanco commented 2 years ago

This is issue is an interesting one. Based on @MichaelJKar reproduction steps I built a fix for this case. Sadly I do not know enough in order to say if this change can have some side effects in another (yet untested) cases.