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

Solution to the @Grab issue #103

Open
wheelerlaw opened this issue Nov 18, 2020 · 0 comments
Open

Solution to the @Grab issue #103

wheelerlaw opened this issue Nov 18, 2020 · 0 comments

Comments

@wheelerlaw
Copy link

wheelerlaw commented Nov 18, 2020

I have figured out a solution to the classloader issue with @Grab. See my answer for this StackOverflow question.

To summarize, because the GrapeIvy.groovy implementation of Grape does not allow classloaders other than groovy.lang.GroovyClassLoader or org.codehaus.groovy.tools.RootLoader, if blows up with a big exception:

Caused by: java.lang.RuntimeException: No suitable ClassLoader found for grab
	at groovy.grape.GrapeIvy.chooseClassLoader(GrapeIvy.groovy:180)
	at groovy.grape.GrapeIvy.grab(GrapeIvy.groovy:247)
	at groovy.grape.Grape.grab(Grape.java:167)

However, I found that it was possible by using the metaclass to override the GrapeIvy.chooseClassLoader with an implementation that allows any classloader:

        GrapeIvy.metaClass.chooseClassLoader = { Map args ->
            def loader = args.classLoader
            if (loader?.class == null) {
                loader = (args.refObject?.class
                        ?: ReflectionUtils.getCallingClass(args.calleeDepth?:1)
                )?.classLoader
                while (loader && loader?.class == null) {
                    loader = loader.parent
                }

                if (loader?.class == null) {
                    throw new RuntimeException("No suitable ClassLoader found for grab")
                }
            }
            return loader
        }

This just needs to run somewhere before the script gets loaded somehow and the @Grab gets executed.

If you are using Junit (or JenkinsPipelineUnit), you probably would want to put this code in a method annotated with @BeforeClass.

If you are using jenkins-spock like I am (instead of JenkinsPipelineUnit), you will need to create a custom Spock global extension (you can't simply use setupSpec because the super class' setupSpec gets executed first and it scans the classpath and loads the scripts, executing the @Grab)

So in my project, added a file test/unit/resources/META-INF/services/org.spockframework.runtime.extension.IGlobalExtension that has a single line, the fully qualified name of the custom global extension class:

org.cvp.extension.GroovyGrapeExtension

Then I have a class called GroovyGrapeExtension that extends org.spockframework.runtime.extension.AbstractGlobalExtension and implements the start() method:

package org.cvp.extension

import groovy.grape.GrapeIvy
import org.codehaus.groovy.reflection.ReflectionUtils
import org.spockframework.runtime.extension.AbstractGlobalExtension

/**
 * This is necessary because by default, {@link GrapeIvy} will not allow @Grab to load classes using anything but the
 * classloader implementations {@link groovy.lang.GroovyClassLoader} or {@link org.codehaus.groovy.tools.RootLoader}.
 * However, the classloader used by Gradle when running unit tests is different, so @Grab will fail with an error
 * unless it is overridden. To make this more difficult, the offending method
 * {@link GrapeIvy#isValidTargetClassLoaderClass} is private, and due to a bug in Groovy
 * (https://issues.apache.org/jira/browse/GROOVY-7368), private methods cannot be overridden using the meta class.
 *
 * I would have overridden this behavior by adding a setupSpec() method to {@link org.cvp.pipeline.MessageUtilsSpec}
 * however {@link com.homeaway.devtools.jenkins.testing.JenkinsPipelineSpecification#setupSpec} gets called first
 * and that is where the script is loaded into the classpath (and the @Grab executed).
 *
 * This extension needed to be written because the extension gets executed before any setupSpec() methods are run.
 * This extension is also global, so it gets run before the initialization of every Spock instance.
 */
class GroovyGrapeExtension extends AbstractGlobalExtension {

    /**
     * The {@link org.spockframework.runtime.extension.IGlobalExtension#start} gets executed at the startup of Spock,
     * before any setupSpec() functions are executed.
     */
    @Override
    void start() {
        GrapeIvy.metaClass.chooseClassLoader = { Map args ->
            def loader = args.classLoader
            if (loader?.class == null) {
                loader = (args.refObject?.class
                        ?: ReflectionUtils.getCallingClass(args.calleeDepth?:1)
                )?.classLoader
                while (loader && loader?.class == null) {
                    loader = loader.parent
                }

                if (loader?.class == null) {
                    throw new RuntimeException("No suitable ClassLoader found for grab")
                }
            }
            return loader
        }
    }
}

So far it is working in my project, and I haven't really had time to explore if something like this could be integrated into the Gradle plugin itself so people don't need to add the boilerplate code to their repository.

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

1 participant