jenkinsci / JenkinsPipelineUnit

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

Stack overflow in tests; possible to disable interception for a class? #585

Open vlad-ivanov-name opened 1 year ago

vlad-ivanov-name commented 1 year ago

Jenkins and plugins versions report

Environment ```text Jenkins: 2.361.3 OS: Linux - 5.10.147+ --- PrioritySorter:4.1.0 ace-editor:1.1 ansicolor:1.0.2 antisamy-markup-formatter:155.v795fb_8702324 apache-httpcomponents-client-4-api:4.5.13-138.v4e7d9a_7b_a_e61 authentication-tokens:1.4 authorize-project:1.4.0 basic-branch-build-strategies:71.vc1421f89888e blueocean:1.25.8 blueocean-autofavorite:1.2.5 blueocean-bitbucket-pipeline:1.25.8 blueocean-commons:1.25.8 blueocean-config:1.25.8 blueocean-core-js:1.25.8 blueocean-dashboard:1.25.8 blueocean-display-url:2.4.1 blueocean-events:1.25.8 blueocean-git-pipeline:1.25.8 blueocean-github-pipeline:1.25.8 blueocean-i18n:1.25.8 blueocean-jwt:1.25.8 blueocean-personalization:1.25.8 blueocean-pipeline-api-impl:1.25.8 blueocean-pipeline-editor:1.25.8 blueocean-pipeline-scm-api:1.25.8 blueocean-rest:1.25.8 blueocean-rest-impl:1.25.8 blueocean-web:1.25.8 bootstrap4-api:4.6.0-5 bootstrap5-api:5.2.1-3 bouncycastle-api:2.26 branch-api:2.1046.v0ca_37783ecc5 build-timeout:1.24 caffeine-api:2.9.3-65.v6a_47d0f4d1fe checks-api:1.8.0 cloudbees-bitbucket-branch-source:791.vb_eea_a_476405b cloudbees-folder:6.758.vfd75d09eea_a_1 command-launcher:90.v669d7ccb_7c31 commons-lang3-api:3.12.0-36.vd97de6465d5b_ commons-text-api:1.10.0-27.vb_fa_3896786a_7 copyartifact:1.47 credentials:1189.vf61b_a_5e2f62e credentials-binding:523.vd859a_4b_122e6 display-url-api:2.3.6 docker-commons:1.21 docker-workflow:528.v7c193a_0b_e67c durable-task:501.ve5d4fc08b0be echarts-api:5.4.0-1 email-ext:2.92 favorite:2.4.1 font-awesome-api:6.2.0-3 gcp-secrets-manager-credentials-provider:0.3.1 generic-webhook-trigger:1.85.2 git:4.13.0 git-client:3.13.0 git-server:99.va_0826a_b_cdfa_d github:1.36.0 github-api:1.303-400.v35c2d8258028 github-branch-source:1696.v3a_7603564d04 github-scm-trait-notification-context:1.1 google-compute-engine:4.3.12 google-login:1.6 google-oauth-plugin:1.0.7 handlebars:3.0.8 handy-uri-templates-2-api:2.1.8-22.v77d5b_75e6953 htmlpublisher:1.31 instance-identity:116.vf8f487400980 ionicons-api:31.v4757b_6987003 jackson2-api:2.13.4.20221013-295.v8e29ea_354141 jakarta-activation-api:2.0.1-2 jakarta-mail-api:2.0.1-2 javax-activation-api:1.2.0-5 javax-mail-api:1.6.2-8 jaxb:2.3.7-1 jdk-tool:63.v62d2fd4b_4793 jenkins-design-language:1.25.8 jjwt-api:0.11.5-77.v646c772fddb_0 jnr-posix-api:3.1.15-2 job-dsl:1.81 jquery3-api:3.6.1-2 jsch:0.1.55.61.va_e9ee26616e7 junit:1156.vcf492e95a_a_b_0 ldap:2.12 lockable-resources:2.18 mailer:438.v02c7f0a_12fa_4 matrix-auth:3.1.5 matrix-project:785.v06b_7f47b_c631 mina-sshd-api-common:2.9.1-44.v476733c11f82 mina-sshd-api-core:2.9.1-44.v476733c11f82 momentjs:1.1.1 oauth-credentials:0.5 okhttp-api:4.9.3-108.v0feda04578cf opentelemetry:2.9.2 pam-auth:1.10 pipeline-build-step:2.18 pipeline-github-lib:38.v445716ea_edda_ pipeline-graph-analysis:195.v5812d95a_a_2f9 pipeline-groovy-lib:613.v9c41a_160233f pipeline-input-step:456.vd8a_957db_5b_e9 pipeline-milestone-step:101.vd572fef9d926 pipeline-model-api:2.2118.v31fd5b_9944b_5 pipeline-model-definition:2.2118.v31fd5b_9944b_5 pipeline-model-extensions:2.2118.v31fd5b_9944b_5 pipeline-rest-api:2.27 pipeline-stage-step:296.v5f6908f017a_5 pipeline-stage-tags-metadata:2.2118.v31fd5b_9944b_5 pipeline-stage-view:2.27 pipeline-utility-steps:2.13.1 plain-credentials:139.ved2b_9cf7587b plugin-util-api:2.18.0 popper-api:1.16.1-3 popper2-api:2.11.6-2 pubsub-light:1.17 resource-disposer:0.20 scm-api:621.vda_a_b_055e58f7 script-security:1189.vb_a_b_7c8fd5fde simple-theme-plugin:136.v23a_15f86c53d slack:629.vf00ea_cb_40d53 snakeyaml-api:1.32-86.ve3f030a_75631 sse-gateway:1.26 ssh-agent:295.v9ca_a_1c7cc3a_a_ ssh-credentials:305.v8f4381501156 ssh-slaves:2.854.v7fd446b_337c9 sshd:3.249.v2dc2ea_416e33 structs:324.va_f5d6774f3a_d throttle-concurrents:2.9 timestamper:1.21 token-macro:308.v4f2b_ed62b_b_16 trilead-api:2.72.v2a_3236754f73 variant:59.vf075fe829ccb workflow-aggregator:590.v6a_d052e5a_a_b_5 workflow-api:1200.v8005c684b_a_c6 workflow-basic-steps:994.vd57e3ca_46d24 workflow-cps:3520.va_8fc49e2f96f workflow-durable-task-step:1210.va_1e5d77e122b workflow-job:1254.v3f64639b_11dd workflow-multibranch:716.vc692a_e52371b_ workflow-scm-step:400.v6b_89a_1317c9a_ workflow-step-api:639.v6eca_cd8c04a_a_ workflow-support:839.v35e2736cfd5c ws-cleanup:0.43 ```

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

The issue occurs only in tests; tests run on MacOS

Reproduction steps

I have the following directory tree:

.
├── pom.xml
├── src
│   └── com
│       └── company
│           ├── Utils.groovy
│           └── SomeClass.groovy
├── test
│   └── PipelineFunctionTest.groovy
├── test-resources
└── vars
    └── pipelineFunction.groovy

With the following pom.xml:

<sourceDirectory>src</sourceDirectory>
<testSourceDirectory>test</testSourceDirectory>
<testResources>
    <testResource>
        <directory>test-resources</directory>
    </testResource>
</testResources>

What I'm trying to accomplish:

1) SomeClass can be imported by pipelineFunction.groovy 2) Utils class has static methods that SomeClass methods can call

Expected Results

I can invoke static methods

Actual Results

I'm getting a stack overflow with the following set of frames repeating:

    at jdk.internal.reflect.GeneratedMethodAccessor56.invoke(Unknown Source)
    at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.base/java.lang.reflect.Method.invoke(Method.java:568)
    at org.codehaus.groovy.reflection.CachedMethod.invoke(CachedMethod.java:93)
    at groovy.lang.MetaMethod.doMethodInvoke(MetaMethod.java:325)
    at groovy.lang.MetaMethod$doMethodInvoke.call(Unknown Source)
    at com.lesfurets.jenkins.unit.PipelineTestHelper.callMethod(PipelineTestHelper.groovy:323)
    at jdk.internal.reflect.GeneratedMethodAccessor52.invoke(Unknown Source)
    at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.base/java.lang.reflect.Method.invoke(Method.java:568)
    at org.codehaus.groovy.reflection.CachedMethod.invoke(CachedMethod.java:93)
    at groovy.lang.MetaMethod.doMethodInvoke(MetaMethod.java:325)
    at groovy.lang.MetaClassImpl.invokeMethod(MetaClassImpl.java:1213)
    at groovy.lang.MetaClassImpl.invokeMethod(MetaClassImpl.java:1022)
    at org.codehaus.groovy.runtime.callsite.PogoMetaClassSite.callCurrent(PogoMetaClassSite.java:69)
    at org.codehaus.groovy.runtime.callsite.AbstractCallSite.callCurrent(AbstractCallSite.java:182)
    at com.lesfurets.jenkins.unit.PipelineTestHelper$_closure3.doCall(PipelineTestHelper.groovy:310)
    at jdk.internal.reflect.GeneratedMethodAccessor44.invoke(Unknown Source)
    at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.base/java.lang.reflect.Method.invoke(Method.java:568)
    at org.codehaus.groovy.reflection.CachedMethod.invoke(CachedMethod.java:93)
    at org.codehaus.groovy.runtime.metaclass.ClosureMetaMethod.invoke(ClosureMetaMethod.java:84)
    at groovy.lang.ExpandoMetaClass.invokeMethod(ExpandoMetaClass.java:1123)
    at groovy.lang.MetaClassImpl.invokeMethod(MetaClassImpl.java:1022)
    at org.codehaus.groovy.runtime.callsite.PogoMetaClassSite.call(PogoMetaClassSite.java:42)
    at org.codehaus.groovy.runtime.callsite.AbstractCallSite.call(AbstractCallSite.java:117)
    at com.lesfurets.jenkins.unit.PipelineTestHelper$_cloneArgs_closure7.doCall(PipelineTestHelper.groovy:456)
    at jdk.internal.reflect.GeneratedMethodAccessor53.invoke(Unknown Source)
    at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.base/java.lang.reflect.Method.invoke(Method.java:568)
    at org.codehaus.groovy.reflection.CachedMethod.invoke(CachedMethod.java:93)
    at groovy.lang.MetaMethod.doMethodInvoke(MetaMethod.java:325)
    at org.codehaus.groovy.runtime.metaclass.ClosureMetaClass.invokeMethod(ClosureMetaClass.java:294)
    at groovy.lang.MetaClassImpl.invokeMethod(MetaClassImpl.java:1022)
    at groovy.lang.Closure.call(Closure.java:414)
    at groovy.lang.Closure.call(Closure.java:430)
    at org.codehaus.groovy.runtime.DefaultGroovyMethods.each(DefaultGroovyMethods.java:2040)
    at org.codehaus.groovy.runtime.DefaultGroovyMethods.each(DefaultGroovyMethods.java:1895)
    at org.codehaus.groovy.runtime.dgm$160.invoke(Unknown Source)
    at org.codehaus.groovy.runtime.callsite.PojoMetaMethodSite$PojoMetaMethodSiteNoUnwrapNoCoerce.invoke(PojoMetaMethodSite.java:274)
    at org.codehaus.groovy.runtime.callsite.PojoMetaMethodSite.call(PojoMetaMethodSite.java:56)
    at org.codehaus.groovy.runtime.callsite.AbstractCallSite.call(AbstractCallSite.java:125)
    at com.lesfurets.jenkins.unit.PipelineTestHelper.cloneArgs(PipelineTestHelper.groovy:449)
    at com.lesfurets.jenkins.unit.PipelineTestHelper.registerMethodCall(PipelineTestHelper.groovy:474)
    at jdk.internal.reflect.GeneratedMethodAccessor46.invoke(Unknown Source)
    at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.base/java.lang.reflect.Method.invoke(Method.java:568)
    at org.codehaus.groovy.reflection.CachedMethod.invoke(CachedMethod.java:93)
    at groovy.lang.MetaMethod.doMethodInvoke(MetaMethod.java:325)
    at groovy.lang.MetaClassImpl.invokeMethod(MetaClassImpl.java:1213)
    at groovy.lang.MetaClassImpl.invokeMethod(MetaClassImpl.java:1022)
    at org.codehaus.groovy.runtime.callsite.PogoMetaClassSite.callCurrent(PogoMetaClassSite.java:69)
    at org.codehaus.groovy.runtime.callsite.AbstractCallSite.callCurrent(AbstractCallSite.java:190)
    at com.lesfurets.jenkins.unit.PipelineTestHelper$_closure3.doCall(PipelineTestHelper.groovy:300)
    at jdk.internal.reflect.GeneratedMethodAccessor44.invoke(Unknown Source)
    at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.base/java.lang.reflect.Method.invoke(Method.java:568)
    at org.codehaus.groovy.reflection.CachedMethod.invoke(CachedMethod.java:93)
    at groovy.lang.MetaMethod.doMethodInvoke(MetaMethod.java:325)
    at org.codehaus.groovy.runtime.metaclass.ClosureMetaClass.invokeMethod(ClosureMetaClass.java:294)
    at groovy.lang.MetaClassImpl.invokeMethod(MetaClassImpl.java:1022)
    at groovy.lang.Closure.call(Closure.java:414)
    at org.codehaus.groovy.runtime.metaclass.ClosureStaticMetaMethod.invoke(ClosureStaticMetaMethod.java:62)
    at groovy.lang.ExpandoMetaClass.invokeStaticMethod(ExpandoMetaClass.java:1136)
    at org.codehaus.groovy.runtime.callsite.StaticMetaClassSite.callStatic(StaticMetaClassSite.java:65)
    at org.codehaus.groovy.runtime.callsite.AbstractCallSite.callStatic(AbstractCallSite.java:206)
    at com.company.SomeClass.someMethod(SomeClass.groovy:19)

Anything else?

I don't actually need to intercept methods in that part of code; is there something I can do to just disable interception there?

vlad-ivanov-name commented 1 year ago

The only workaround I found is to convert SomeClass and Utils to Java. But it's not a very good solution, I would certainly prefer to have them in groovy as well.

axieum commented 4 months ago

It appears to be caused by having two methods with the same name in src/ and vars/, regardless of their class hierarchy.

// src/com/example/HardMath.groovy
package com.example

class HardMath implements Serializable {
  Object script = null

  int complexOperation(int a, int b) {
    script.echo "Adding ${a} to ${b}"
    return a + b
  }
}
// vars/complexOperation.groovy
import com.example.HardMath

int call(int a, int b) {
  return new HardMath(script: this).complexOperation(a, b)
}

In this example, you can see the complexOperation symbol is defined twice. After debugging, the interceptor is redirecting the HardMath#complexOperation call back to the step defined in vars causing recursion.

axieum commented 4 months ago

https://github.com/axieum/jenkinspipelineunit-issue-585

I've pushed up a reproduction (bare minimum) of this StackOverflowError, simply clone the repository and run ./gradlew test.

axieum commented 4 months ago

It appears our global steps defined in vars/ are loaded into the helper#allowedMethodCallbacks first - this is important later.

The src/ path is added to the Groovy class loader. https://github.com/jenkinsci/JenkinsPipelineUnit/blob/70482cb5ad4027b5eb585abe62b38ba176975cdc/src/main/groovy/com/lesfurets/jenkins/unit/global/lib/LibraryLoader.groovy#L106-L111

The InterceptingGCL Groovy class loader overrides the #parseClass method to intercept method calls, https://github.com/jenkinsci/JenkinsPipelineUnit/blob/70482cb5ad4027b5eb585abe62b38ba176975cdc/src/main/groovy/com/lesfurets/jenkins/unit/InterceptingGCL.groovy#L55-L61

..which in turn replaces our class method call with a reference to the already registered global step - causing recursion: https://github.com/jenkinsci/JenkinsPipelineUnit/blob/70482cb5ad4027b5eb585abe62b38ba176975cdc/src/main/groovy/com/lesfurets/jenkins/unit/InterceptingGCL.groovy#L18-L26

So, regardless of whether complexOperation#call(int, int) or com.example.HardMath#complexOperation(int, int) is called, we can see it looks up against the complexOperation name only which just so happens to match a step defined in helper#allowedMethodCallbacks.