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

Cannot cast object to an interface type during test #679

Open
wiktorinox opened this issue Sep 4, 2024 · 3 comments
Open

Cannot cast object to an interface type during test #679

wiktorinox opened this issue Sep 4, 2024 · 3 comments

Comments

@wiktorinox
Copy link

Hello everyone. I was using this framework to create unit tests for newly created Jenkins shared library. I was pretty happy with this until I've encountered a problem that is outlined below - I would greatly appreciate any help as I'm currently at my wits ends.

High level description:

I'm experiencing a problem during test execution: I'm trying to create an instance of an object and then cast it to the interface type that this class implements. I'm getting GroovyCastException. The same code works correctly on Jenkins.

Details:

Environment:

Gradle 8.5
JVM: 11.0.24 (Ubuntu 11.0.24+8-post-Ubuntu-1ubuntu320.04)

I've distilled the problem to the smallest reproducible setup.
Let's assume following project structure:

├── build.gradle
├── gradle.properties
├── settings.gradle
├── src
│   └── com
│       └── example
│           └── pkg
│               ├── AFoo.groovy
│               └── IFoo.groovy
├── test
│   └── groovy
│       └── vars
│           └── HelloWorldTest.groovy
└── vars
    └── helloWorld.groovy

AFoo.groovy:

package com.example.pkg

class AFoo implements IFoo {
    void bar() {
        println "test"
    }
}

IFoo.groovy:

package com.example.pkg

interface IFoo {
    void bar()
}

helloWorld.groovy:

import com.example.pkg.AFoo
import com.example.pkg.IFoo

void call() {
    IFoo foo = getFoo()
    echo "Hello world"
}

IFoo getFoo() {
    AFoo af = new AFoo()
    return af
}

I'm loading the current library during the tests and trying to execute helloWorld step in HelloWorldTest.groovy:

import static com.lesfurets.jenkins.unit.global.lib.LibraryConfiguration.library
import static com.lesfurets.jenkins.unit.global.lib.ProjectSource.projectSource
import com.lesfurets.jenkins.unit.BasePipelineTest
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.TestInstance

class HelloWorldTest extends BasePipelineTest {

    @Override
    @BeforeEach
    void setUp() {
        scriptRoots += 'vars'
        Object library = library()
            .name('solaris')
            .defaultVersion('<notNeeded>')
            .allowOverride(true)
            .implicit(true)
            .targetPath('<notNeeded>')
            .retriever(projectSource())
            .build()
        helper.registerSharedLibrary(library)
        super.setUp()
    }

    @Test
    void testHello() {
        String expectedEcho = "Hello world"
        Script script = loadScript('helloWorld.groovy')
        script.call()
        printCallStack()
        assertCallStackContains(expectedEcho)
        assertJobStatusSuccess()
    }

}

My build.gradle:

plugins {
    // Apply the groovy Plugin to add support for Groovy.
    id 'groovy'

    // Apply the java-library plugin for API and implementation separation.
    id 'java-library'
}

repositories {
    // Use Maven Central for resolving dependencies.
    mavenCentral()
    maven { url 'https://repo.jenkins-ci.org/releases/' }
}

dependencies {
    implementation 'org.codehaus.groovy:groovy:2.4.21'
    implementation 'org.codehaus.groovy:groovy-all:2.4.21'

    testImplementation platform("org.junit:junit-bom:5.11.0")
    testImplementation 'org.junit.jupiter:junit-jupiter-api'
    testImplementation 'org.junit.jupiter:junit-jupiter-params'
    testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine'
    testImplementation 'com.lesfurets:jenkins-pipeline-unit:1.20'
}

sourceSets {
    // Location where gradle should look for files to build
    main {
        groovy {
            srcDirs = ['src', 'vars']
        }
    }
    test {
        groovy {
            srcDirs = ['test']
        }
    }
}

test {
    useJUnitPlatform()

    maxHeapSize = '1G'

    testLogging {
        events 'passed'
        exceptionFormat 'full'
    }

    // delete old test reports
    dependsOn 'cleanTest'

    afterSuite { desc, result ->
        if(!desc.parent) {
            println "\nRESULTS > ${result.resultType} (${result.testCount} tests, ${result.successfulTestCount} passed, ${result.failedTestCount} failed, ${result.skippedTestCount} skipped)"
        }
    }
}

buildDir = new File("${getLayout().getProjectDirectory()}/../${project.name}_build")

If I run gradle :test I'm getting following error:

gradle :test
                                                          
WARNING: An illegal reflective access operation has occurred
WARNING: Illegal reflective access by org.codehaus.groovy.reflection.CachedClass (file:/home/wbrozyn/.gradle/caches/modules-2/files-2.1/org.codehaus.groovy/groovy/2.4.21/abbb8d83268f3243a57d9f7768e159373a4e378/groovy-2.4.21.jar) to method java.lang.Object.finalize()
WARNING: Please consider reporting this to the maintainers of org.codehaus.groovy.reflection.CachedClass
WARNING: Use --illegal-access=warn to enable warnings of further illegal reflective access operations
WARNING: All illegal access operations will be denied in a future release

> Task :test

HelloWorldTest > testHello() FAILED
    org.codehaus.groovy.runtime.typehandling.GroovyCastException: Cannot cast object 'com.example.pkg.AFoo@4e2916c3' with class 'com.example.pkg.AFoo' to class 'com.example.pkg.IFoo'
        at org.codehaus.groovy.runtime.typehandling.DefaultTypeTransformation.continueCastOnSAM(DefaultTypeTransformation.java:418)
        at org.codehaus.groovy.runtime.typehandling.DefaultTypeTransformation.continueCastOnNumber(DefaultTypeTransformation.java:332)
        at org.codehaus.groovy.runtime.typehandling.DefaultTypeTransformation.castToType(DefaultTypeTransformation.java:245)
        at org.codehaus.groovy.runtime.ScriptBytecodeAdapter.castToType(ScriptBytecodeAdapter.java:616)
        at helloWorld.call(helloWorld.groovy:5)
        at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
        at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
        at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
        at java.base/java.lang.reflect.Method.invoke(Method.java:566)
        at org.codehaus.groovy.reflection.CachedMethod.invoke(CachedMethod.java:98)
        at groovy.lang.MetaMethod.doMethodInvoke(MetaMethod.java:325)
        at groovy.lang.MetaMethod$doMethodInvoke.call(Unknown Source)
        at org.codehaus.groovy.runtime.callsite.CallSiteArray.defaultCall(CallSiteArray.java:47)
        at org.codehaus.groovy.runtime.callsite.AbstractCallSite.call(AbstractCallSite.java:116)
        at org.codehaus.groovy.runtime.callsite.AbstractCallSite.call(AbstractCallSite.java:136)
        at com.lesfurets.jenkins.unit.PipelineTestHelper.callMethod(PipelineTestHelper.groovy:329)
        at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
        at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
        at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
        at java.base/java.lang.reflect.Method.invoke(Method.java:566)
        at org.codehaus.groovy.reflection.CachedMethod.invoke(CachedMethod.java:98)
        at groovy.lang.MetaMethod.doMethodInvoke(MetaMethod.java:325)
        at groovy.lang.MetaClassImpl.invokeMethod(MetaClassImpl.java:1225)
        at groovy.lang.MetaClassImpl.invokeMethod(MetaClassImpl.java:1034)
        at org.codehaus.groovy.runtime.callsite.PogoMetaClassSite.callCurrent(PogoMetaClassSite.java:68)
        at org.codehaus.groovy.runtime.callsite.CallSiteArray.defaultCallCurrent(CallSiteArray.java:51)
        at org.codehaus.groovy.runtime.callsite.AbstractCallSite.callCurrent(AbstractCallSite.java:157)
        at org.codehaus.groovy.runtime.callsite.AbstractCallSite.callCurrent(AbstractCallSite.java:185)
        at com.lesfurets.jenkins.unit.PipelineTestHelper$_closure3.doCall(PipelineTestHelper.groovy:316)
        at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
        at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
        at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
        at java.base/java.lang.reflect.Method.invoke(Method.java:566)
        at org.codehaus.groovy.reflection.CachedMethod.invoke(CachedMethod.java:98)
        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:1034)
        at org.codehaus.groovy.runtime.callsite.PogoMetaClassSite.call(PogoMetaClassSite.java:41)
        at org.codehaus.groovy.runtime.callsite.CallSiteArray.defaultCall(CallSiteArray.java:47)
        at org.codehaus.groovy.runtime.callsite.AbstractCallSite.call(AbstractCallSite.java:116)
        at org.codehaus.groovy.runtime.callsite.AbstractCallSite.call(AbstractCallSite.java:120)
        at HelloWorldTest.testHello(HelloWorldTest.groovy:30)

RESULTS > FAILURE (1 tests, 0 passed, 1 failed, 0 skipped)

1 test completed, 1 failed

> Task :test FAILED

I've tried many things - none of which worked: using normal inheritance (instead of using interfaces), moving interface to a different package, trying few different versions of groovy or this library (in a few combinations), and few others that I can't recall right now.

Conversely this happens on groovy 2.*, on Groovy 3.0.13 which I initially used to compile and run tests with it doesn't error - it hangs on parseClass in GroovyClassLoader.

I would really appreciate any help - either in solving this error outlined above or in getting this to run with Groovy 3.0.13 (which I would prefer but I know that you have no reason to support that).

Thank you in advance.

@nre-ableton
Copy link
Contributor

On first glance, that seems like a really weird error. I'm not exactly sure why that is happening here.

My second question would be, since you have gone to the bother of having a properly structured pipeline library, why you would bother testing it by loading the library and then running loadScript in each unit test. Why not just test the individual class methods (in this case, AFoo.bar) directly in the unit tests?

I realize of course that this is a simplified example and your use case may be much different (and you also may not be able to share the code for said use case), but I'll just say that at my company we don't use JenkinsPipelineUnit to test such things, either complete Jenkinsfiles or the singletons. We've intentionally put all testable logic in classes and only test those classes; the singleton functions are (with very few exceptions) simple passthru methods that don't necessitate unit tests.

@wiktorinox
Copy link
Author

Thanks for the response.

You are right, actual code that I have problem with is bit more complex and property of the company I work for, so I had to prepare an example of a problem that is easy to understand and replicate.

To answer your question about the testing part - I'm of opinion that it's best to use the same API for testing as the one that will be used by its users. In the case of the shared library it would be mostly in vars. That way we can minimize coupling between tests and the implementation. It also helps when you write in TDD, as you can just more or less decide on the interface and then after all tests are passing, you can change implementation as you wish.

We were able to write quite a number of tests that way with implementations that are ranging between usage of src classes and code put into the vars files. I was about to start writing parts of implementation that was more complex (with interfaces and inheritance) and run into the problem described above.

To sum up: I'm not really able to change the approach to testing right now, and I would like to be able to use inheritance and interface in the implementations. That means I will have to figure it out myself.

I think this problem has something to do with custom implementation of GroovyClassLoader that is in the JenkinsPipelineUnit, because if I write a script that just load those classes using default GCL then calling them works just fine. Or maybe there is something else going on later during execution. I will let you know if I arrive at any conclusions.

(Sorry for late response, I was on a vacation)

@wiktorinox
Copy link
Author

Ok, I was wrong about the custom implementation of GroovyClassLoader.

I found out that the problem disappears if I turn off library class preloading. However, this is not an acceptable solution - in my actual code I depend on being able to access Jenkins variables and steps all over the place - which stops working immediately when I disable preloading.

Any idea how this code in the LibraryLoader.groovy
(lines 129 - 136)

                if (preloadLibraryClasses && srcPath.toFile().exists()) {
                    srcPath.toFile().eachFileRecurse (FILES) { File srcFile ->
                        if (srcFile.name.endsWith(".groovy")) {
                            Class clazz = groovyClassLoader.parseClass(srcFile)
                            groovyClassLoader.loadClass(clazz.name)
                        }
                    }
                }

could be causing problems?

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

2 participants