Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Params not accessible in SharedLibrary classes #478

Open
MichaelJKar opened this issue Feb 4, 2022 · 6 comments
Open

Params not accessible in SharedLibrary classes #478

MichaelJKar opened this issue Feb 4, 2022 · 6 comments

Comments

@MichaelJKar
Copy link

MichaelJKar commented Feb 4, 2022

Jenkins and plugins versions report

Environment
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
Copy link

elora-walmisley commented Mar 8, 2022

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
Copy link
Contributor

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

@elora-walmisley
Copy link

@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
Copy link

svyotov commented Mar 22, 2022

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
Copy link
Contributor

nre-ableton commented Mar 24, 2022

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
Copy link
Contributor

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

5 participants