使用metaClass模拟Gradle project.exec {...} [英] Mock Gradle project.exec {...} using metaClass

查看:135
本文介绍了使用metaClass模拟Gradle project.exec {...}的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

作为测试Gradle插件的一部分,我想列出一个常规方法: project.exec {...} 。这是为了确认它正在进行正确的命令行调用。我正在尝试使用元编程:

$ p $ Project proj = ProjectBuilder.builder()。build()

proj.metaClass.exec = {Closure obj - >
println'MOCK EXEC'
}

proj.exec {
可执行'echo'
args'PROJECT EXEC'
}
//打印'PROJECT EXEC'而不是'MOCK EXEC'我预计

好奇的是如果我将 exec 方法重命名为 othername ,那么它可以正常工作:

  Project proj = ProjectBuilder.builder()。build()

proj.metaClass.othername = {Closure obj - >
println'MOCK EXEC'
}

proj.othername {
可执行'echo'
args'PROJECT EXEC'
}
//按预期打印'MOCK EXEC'

我试图弄清为什么现有的 project.exec 方法会导致元编程失败,如果有解决方法。请注意, Project 是一个接口,但我正在模拟 DefaultProject 的特定实例。



用于存储单一方法的元编程方法来自以下答案: https://stackoverflow.com / a / 23818476/1509221

解决方案

在Groovy中,使用metaClass替换界面中定义的方法已经失效。在这种情况下, exec 方法在 Project 类中定义,该类是一个接口。从 GROOVY-3493 (最初于2009年报告):

 无法通过元类覆盖属于接口实现的方法

解决方法



invokeMethod 所有的方法,并可以工作。这是矫枉过正,但它确实有效。当方法名称匹配 exec 时,它会将调用转移到 mySpecialInstance 对象。否则它会传递给委托,即现有的方法。感谢 invokeMethod代表团记录所有方法输入这个。

  //这将拦截所有方法,将exec存入并传递所有其他调用
this.project.metaClass.invokeMethod = {String name,args - >
if(name =='exec'){
//调用特殊实例来跟踪验证
mySpecialInstance.exec((Closure)args.first())
} else {
//这会调用委托而不会导致无限递归
MetaMethod metaMethod = delegate.class.metaClass.getMetaMethod(name,args)
返回metaMethod?.invoke(delegate,args)
}
}

除了您可能会看到数量错误参数或无法在空对象上调用方法xxxxx。问题是上面的代码不处理强制方法参数。对于 project.files(Object ... paths),invokeMethod的参数应该是 [['path1','path2' ] 。但是,在某些情况下,调用 files(null) files(),所以invokeMethod的参数变为分别为 [null] [] ,因为期望 [[]] ] 。产生上述错误。

下面的代码只为文件方法解决了这个问题,但这足以满足我的单位试验。我仍然希望找到更好的强制类型或更换单个方法的方法。

  //如上所述,但是处理胁迫文件参数类型
this.project.metaClass.invokeMethod = {String name,args - >
if(name =='exec'){
//调用特殊实例来跟踪验证
mySpecialInstance.exec((Closure)args.first())
} else {
//这会调用委托而不会导致无限递归
// https://stackoverflow.com/a/10126006/1509221
MetaMethod metaMethod = delegate.class.metaClass.getMetaMethod(name, args)
logInvokeMethod(name,args,metaMethod)

特殊情况'files'方法可以抛出异常
if(name =='files'){
//强制参数以匹配Project.files(Object ...路径)的签名
// TODO:是否有自动执行此操作的方法,例如coerceArgumentsToClasses?
assert 0 == args.size()|| 1 == args.size()

if(args.size()== 0 || // files()
args.first()== null){//文件(null)
返回metaMethod?.invoke(delegate,[[] as Object []] Object [])
} else {
//文件(ArrayList)可能,所以抛出ArrayList to Object []
返回metaMethod?.invoke(delegate,[(Object [])args.first()] as Object [])
}
} else {
/ / Normal pass through
return metaMethod?.invoke(delegate,args)
}
}
}


As part of testing a Gradle plugin, I would like to stub out a groovy method: project.exec {...}. This is to confirm it's making the correct command line calls. I was attempting this using metaprogramming:

Project proj = ProjectBuilder.builder().build()

proj.metaClass.exec = { Closure obj ->
    println 'MOCK EXEC'
}

proj.exec {
    executable 'echo'
    args 'PROJECT EXEC'
}
// prints 'PROJECT EXEC' instead of the 'MOCK EXEC' I expected

What's curious is that if I rename both exec methods to othername, then it works correctly:

Project proj = ProjectBuilder.builder().build()

proj.metaClass.othername = { Closure obj ->
    println 'MOCK EXEC'
}

proj.othername {
    executable 'echo'
    args 'PROJECT EXEC'
}
// prints 'MOCK EXEC' as expected

I'm trying to figure out why the existing project.exec method causes the metaprogramming to fail and if there's a workaround. Note that Project is an interface but I'm mocking a specific instance of type DefaultProject.

The metaprogramming method for stubbing out a single method is from this answer: https://stackoverflow.com/a/23818476/1509221

解决方案

In Groovy replacing a method defined in an interface using metaClass is broken. In this case, the exec method is defined in the Project class, which is an interface. From GROOVY-3493 (reported originally in 2009):

"Cannot override methods via metaclass that are part of an interface implementation"

WORKAROUND

invokeMethod intercepts all methods and can work. This is overkill but it does work. When the method name matches exec, it diverts the call to the mySpecialInstance object. Otherwise it's passed through to the delegate, namely the existing methods. Thanks to invokeMethod delegation and Logging All Methods for input on this.

// This intercepts all methods, stubbing out exec and passing through all other invokes
this.project.metaClass.invokeMethod = { String name, args ->
    if (name == 'exec') {
        // Call special instance to track verifications
        mySpecialInstance.exec((Closure) args.first())
    } else {
        // This calls the delegate without causing infinite recursion
        MetaMethod metaMethod = delegate.class.metaClass.getMetaMethod(name, args)
        return metaMethod?.invoke(delegate, args)
    }
}

This works well except that you may see exceptions about "wrong number of arguments" or "Cannot invoke method xxxxx on null object". The problem is that the above code doesn't handle coercing of the method arguments. For the project.files(Object... paths), the args for invokeMethod should be of the form [['path1', 'path2']]. BUT, in some cases there's a call to files(null) or files() so the args for invokeMethod turn out to be [null] and [] respectively, which fail as it's expecting [[]]. Producing the aforementioned errors.

The following code only solves this for the files method but that was sufficient for my unit tests. I would still like to find a better way of coercing types or ideally of replacing a single method.

// As above but handle coercing of the files parameter types
this.project.metaClass.invokeMethod = { String name, args ->
    if (name == 'exec') {
        // Call special instance to track verifications
        mySpecialInstance.exec((Closure) args.first())
    } else {
        // This calls the delegate without causing infinite recursion
        // https://stackoverflow.com/a/10126006/1509221
        MetaMethod metaMethod = delegate.class.metaClass.getMetaMethod(name, args)
        logInvokeMethod(name, args, metaMethod)

        // Special case 'files' method which can throw exceptions
        if (name == 'files') {
            // Coerce the arguments to match the signature of Project.files(Object... paths)
            // TODO: is there a way to do this automatically, e.g. coerceArgumentsToClasses?
            assert 0 == args.size() || 1 == args.size()

            if (args.size() == 0 ||  // files()
                args.first() == null) {  // files(null)
                return metaMethod?.invoke(delegate, [[] as Object[]] as Object[])
            } else {
                // files(ArrayList) possibly, so cast ArrayList to Object[]
                return metaMethod?.invoke(delegate, [(Object[]) args.first()] as Object[])
            }
        } else {
            // Normal pass through 
            return metaMethod?.invoke(delegate, args)
        }
    }
}

这篇关于使用metaClass模拟Gradle project.exec {...}的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆