使用metaClass模拟Gradle project.exec {...}

bru*_*den 5 groovy gradle

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

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
Run Code Online (Sandbox Code Playgroud)

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

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
Run Code Online (Sandbox Code Playgroud)

我试图找出为什么现有project.exec方法导致元编程失败以及是否有解决方法.请注意,这Project是一个接口,但我正在嘲笑一个特定的类型实例DefaultProject.

用于删除单个方法的元编程方法来自这个答案:https://stackoverflow.com/a/23818476/1509221

bru*_*den 1

在 Groovy 中,使用元类替换接口中定义的方法已被破坏。在这种情况下,该exec方法是在Project类中定义的,该类是一个接口。来自GROOVY-3493(最初于 2009 年报告):

"Cannot override methods via metaclass that are part of an interface implementation"
Run Code Online (Sandbox Code Playgroud)

解决方法

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

// 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)
    }
}
Run Code Online (Sandbox Code Playgroud)

除了您可能会看到有关“参数数量错误”或“无法在空对象上调用方法 xxxxx”的异常之外,这种方法效果很好。问题是上面的代码不处理方法参数的强制。对于project.files(Object... paths),invokeMethod 的参数应采用以下形式[['path1', 'path2']]。但是,在某些情况下,有一个调用files(null)files()因此 invokeMethod 的参数分别是[null][],但它们会按照预期失败[[]]。产生上述错误。

以下代码仅解决了该files方法的问题,但这对于我的单元测试来说已经足够了。我仍然想找到一种更好的方法来强制类型或理想情况下替换单个方法。

// 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
        // /sf/answers/708820451/
        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)
        }
    }
}
Run Code Online (Sandbox Code Playgroud)