ExpediaGroup / jenkins-spock

Unit-test Jenkins pipeline code with Spock
https://javadoc.io/doc/com.homeaway.devtools.jenkins/jenkins-spock
Apache License 2.0
187 stars 76 forks source link

Tests fail to run with Jenkins 2.357 and up #143

Open mikedld opened 1 year ago

mikedld commented 1 year ago

Expected Behavior

Test run with the most recent versions of org.jenkins-ci.main:jenkins-core package.

Actual Behavior

When using org.jenkins-ci.main:jenkins-core version 2.356 and below, tests run as expected.

When using org.jenkins-ci.main:jenkins-core version 2.357 up to 2.365, running tests produces an error for every test case:

  java.lang.UnsupportedOperationException
      at net.sf.cglib.asm.$ClassVisitor.visitNestMemberExperimental(ClassVisitor.java:248)
      at net.sf.cglib.asm.$ClassReader.accept(ClassReader.java:651)
      at net.sf.cglib.asm.$ClassReader.accept(ClassReader.java:391)
      at net.sf.cglib.proxy.BridgeMethodResolver.resolveAll(BridgeMethodResolver.java:70)
      at net.sf.cglib.proxy.Enhancer.emitMethods(Enhancer.java:1132)
      at net.sf.cglib.proxy.Enhancer.generateClass(Enhancer.java:630)
      at net.sf.cglib.core.DefaultGeneratorStrategy.generate(DefaultGeneratorStrategy.java:25)
      at net.sf.cglib.core.AbstractClassGenerator.generate(AbstractClassGenerator.java:329)
      at net.sf.cglib.proxy.Enhancer.generate(Enhancer.java:492)
      at net.sf.cglib.core.AbstractClassGenerator$ClassLoaderData$3.apply(AbstractClassGenerator.java:93)
      at net.sf.cglib.core.AbstractClassGenerator$ClassLoaderData$3.apply(AbstractClassGenerator.java:91)
      at net.sf.cglib.core.internal.LoadingCache$2.call(LoadingCache.java:54)
      at java.base/java.util.concurrent.FutureTask.run(FutureTask.java:264)
      at net.sf.cglib.core.internal.LoadingCache.createEntry(LoadingCache.java:61)
      at net.sf.cglib.core.internal.LoadingCache.get(LoadingCache.java:34)
      at net.sf.cglib.core.AbstractClassGenerator$ClassLoaderData.get(AbstractClassGenerator.java:116)
      at net.sf.cglib.core.AbstractClassGenerator.create(AbstractClassGenerator.java:291)
      at net.sf.cglib.proxy.Enhancer.createHelper(Enhancer.java:480)
      at net.sf.cglib.proxy.Enhancer.createClass(Enhancer.java:337)
      at org.spockframework.mock.runtime.CglibMockFactory.createMock(CglibMockFactory.java:32)
      at org.spockframework.mock.runtime.ProxyBasedMockFactory.create(ProxyBasedMockFactory.java:45)
      at org.spockframework.mock.runtime.JavaMockFactory.createInternal(JavaMockFactory.java:58)
      at org.spockframework.mock.runtime.JavaMockFactory.create(JavaMockFactory.java:38)
      at org.spockframework.mock.runtime.CompositeMockFactory.create(CompositeMockFactory.java:42)
      at org.spockframework.lang.SpecInternals.createMock(SpecInternals.java:46)
      at org.spockframework.lang.SpecInternals.createMockImpl(SpecInternals.java:294)
      at org.spockframework.lang.SpecInternals.createMockImpl(SpecInternals.java:284)
      at org.spockframework.lang.SpecInternals.MockImpl(SpecInternals.java:108)
      at com.homeaway.devtools.jenkins.testing.JenkinsPipelineSpecification.makeStaticJenkins(JenkinsPipelineSpecification.groovy:1054)
      at com.homeaway.devtools.jenkins.testing.JenkinsPipelineSpecification.getStaticJenkins(JenkinsPipelineSpecification.groovy:1070)
      at com.homeaway.devtools.jenkins.testing.JenkinsPipelineSpecification$1.getInstance(JenkinsPipelineSpecification.groovy:1103)
      at jenkins.model.Jenkins.getInstanceOrNull(Jenkins.java:844)
      at hudson.cli.CLICommand.<clinit>(CLICommand.java:585)
      at java.base/java.lang.Class.forName(Class.java:315)
      at com.homeaway.devtools.jenkins.testing.WholeClasspathPipelineExtensionDetector.getClassesWithAnnotationOfTypeInPackage(WholeClasspathPipelineExtensionDetector.java:112)
      at com.homeaway.devtools.jenkins.testing.APipelineExtensionDetector.getPipelineSteps(APipelineExtensionDetector.java:81)
      at com.homeaway.devtools.jenkins.testing.JenkinsPipelineSpecification.setupSpec(JenkinsPipelineSpecification.groovy:1109)

When using org.jenkins-ci.main:jenkins-core version 2.366 up to 2.395 (current latest), running tests produces an error for every test case:

  java.lang.NullPointerException: Cannot invoke method getMetaMethod() on null object
      at com.homeaway.devtools.jenkins.testing.JenkinsPipelineSpecification.addPipelineMocksToObjects_closure1(JenkinsPipelineSpecification.groovy:609)
      at groovy.lang.Closure.call(Closure.java:420)
      at groovy.lang.Closure.call(Closure.java:436)
      at com.homeaway.devtools.jenkins.testing.JenkinsPipelineSpecification.addPipelineMocksToObjects(JenkinsPipelineSpecification.groovy:607)
      at com.homeaway.devtools.jenkins.testing.JenkinsPipelineSpecification.setup(JenkinsPipelineSpecification.groovy:1175)

Steps to Reproduce

Current working dependencies (reduced from full set required for my project), bumping the version of org.jenkins-ci.main:jenkins-core leads to failures:

repositories {
    mavenCentral()
    maven { url 'https://repo.jenkins-ci.org/releases' }
    maven { url 'https://repo.jenkins-ci.org/public' }
}

dependencies {
    Closure withoutIcu = { exclude group: 'com.ibm.icu', module: 'icu4j' }

    implementation 'org.codehaus.groovy:groovy-all:2.4.21'
    implementation 'org.jenkins-ci.main:jenkins-core:2.332.1', withoutIcu

    implementation 'com.cloudbees:groovy-cps:1.32@jar', withoutIcu
    implementation 'org.jenkinsci.plugins:pipeline-model-definition:2.2075.vce74e77b_ce40@jar'

    implementation 'org.jenkins-ci.plugins.workflow:workflow-api:1143.v2d42f1e9dea_5@jar'
    implementation 'org.jenkins-ci.plugins.workflow:workflow-cps:2686.v7c37e0578401@jar'
    implementation 'org.jenkins-ci.plugins.workflow:workflow-step-api:622.vb_8e7c15b_c95a_@jar'

    testImplementation 'com.homeaway.devtools.jenkins:jenkins-spock:2.1.5'
    testImplementation 'javax.servlet:javax.servlet-api:4.0.1'
    testImplementation 'org.spockframework:spock-core:1.3-groovy-2.4'
}

Minimal failing test:

package foo.bar

import com.homeaway.devtools.jenkins.testing.JenkinsPipelineSpecification

class FooBarSpec extends JenkinsPipelineSpecification {

    def 'foobar works'() {
    expect:
        1 == 1
    }
}

Additional Information

$ java -version
openjdk version "11.0.18" 2023-01-17
OpenJDK Runtime Environment Temurin-11.0.18+10 (build 11.0.18+10)
OpenJDK 64-Bit Server VM Temurin-11.0.18+10 (build 11.0.18+10, mixed mode)
$ ./gradlew -version

------------------------------------------------------------
Gradle 7.3.3
------------------------------------------------------------

Build time:   2021-12-22 12:37:54 UTC
Revision:     6f556c80f945dc54b50e0be633da6c62dbe8dc71

Kotlin:       1.5.31
Groovy:       3.0.9
Ant:          Apache Ant(TM) version 1.10.11 compiled on July 10 2021
JVM:          11.0.18 (Eclipse Adoptium 11.0.18+10)
OS:           Linux 6.2.7-[REDACTED] amd64
mikedld commented 1 year ago

Worked around second error (NullPointerException) by applying the following patch (not sure how good it is, let me know if you want me to open a PR):

--- a/src/main/groovy/com/homeaway/devtools/jenkins/testing/JenkinsPipelineSpecification.groovy
+++ b/src/main/groovy/com/homeaway/devtools/jenkins/testing/JenkinsPipelineSpecification.groovy
@@ -606,6 +606,10 @@ public abstract class JenkinsPipelineSpecification extends Specification {

         _objects.each { object ->

+            if( object.metaClass == null ) {
+                return;
+            }
+
             final MetaMethod originalMethodMissing = object.metaClass.getMetaMethod("methodMissing", "string", new Object[0] )

             object.metaClass.methodMissing = { String _name, _args ->

With the above patch applied, still fails with the first error (UnsupportedOperationException), which could be worked around by bumping the cglib version from 3.2.7 to at least 3.2.9:

dependencies {
    // ...
    testImplementation 'cglib:cglib:3.2.9'
    // ...
}
sdoeringNew commented 11 months ago

In Groovy every class has a metaClass.

But a class can obfuscate the call to metaClass if it has a method with this signature: get(String key) Like org.apache.log4j.MDC and org.slf4j.MDC have. :wink:

The fix is not to check for a null, but to simply use object.getMetaClass() instead of object.metaClass.

sdoeringNew commented 11 months ago

I was able to workaround this issue by adding a Helper class to the test folder having a static initializer:

public class OutsmartJenkinsPipelineSpecification {
    static {
        def string = "metaClass"
        string.metaClass.getMetaMethod { String it1, String it2, Object[] it3 -> }
        string.metaClass.methodMissing = "methodMissing"
        string.metaClass.propertyMissing = "propertyMissing"
        org.slf4j.MDC.put("metaClass", string)
    }
}

That adds a fake metaClass to the org.slf4j.MDC class. (Be aware if you are using MDC while running the tests.)