2015-06-30 9 views
5

Als Teil der Prüfung eines Gradle-Plugins möchte ich eine groovige Methode ausdrücken: project.exec {...}. Hiermit wird bestätigt, dass die richtigen Befehlszeilenaufrufe ausgeführt werden. Ich habe versucht, dies mit metaprogramming:Mock Gradle project.exec {...} mit metaClass

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 

Was neugierig ist, dass, wenn ich beide exec Methoden othername umbenennen, dann funktioniert es richtig:

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 

Ich versuche, herauszufinden, warum die bestehenden project.exec Die Methode führt dazu, dass die Metaprogrammierung fehlschlägt und es eine Problemumgehung gibt. Beachten Sie, dass Project eine Schnittstelle ist, aber ich spotte eine bestimmte Instanz des Typs DefaultProject.

Die metaprogramming Verfahren zur Herstellung einer einzelne Methode Ausdrücken sind aus dieser Antwort: https://stackoverflow.com/a/23818476/1509221

Antwort

1

in der starken in einer Schnittstelle definiert, ein Verfahren unter Verwendung von Ersatz-Metaklasse gebrochen ist. In diesem Fall ist das exec-Verfahren in der Project-Klasse definiert, bei der es sich um eine Schnittstelle handelt. Von GROOVY-3493 (berichtet ursprünglich im Jahr 2009):

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

Abhilfe

invokeMethod fängt alle Methoden und arbeiten können. Das ist Overkill, aber es funktioniert. Wenn der Methodenname mit exec übereinstimmt, wird der Aufruf an das Objekt mySpecialInstance umgeleitet. Andernfalls wird es an den Delegaten übergeben, nämlich die vorhandenen Methoden. Danke an invokeMethod delegation und Logging All Methods für die Eingabe auf diesem.

// 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) 
    } 
} 

Das funktioniert außer gut, dass Sie Ausnahmen über „falsche Anzahl von Argumenten“ sehen kann oder „Can not Methode xxxxx auf Null-Objekt aufrufen“. Das Problem besteht darin, dass der obige Code das Anwenden der Methodenargumente nicht verarbeitet. Für die project.files(Object... paths) sollten die Argumente für invokeMethod das Format [['path1', 'path2']] haben. ABER, in einigen Fällen gibt es einen Anruf an files(null) oder files(), so dass die Argumente für invokeMethod [null] bzw. [] sind, die fehlschlagen, da es [[]] erwartet. Herstellung der oben genannten Fehler.

Der folgende Code löst nur das für die files Methode, aber das war ausreichend für meine Komponententests. Ich möchte immer noch einen besseren Weg finden, Arten zu erzwingen oder idealerweise eine einzige Methode zu ersetzen.

// 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) 
     } 
    } 
} 
+0

Ich bin nicht 100% sicher, dass es funktioniert, aber Sie können möglicherweise die Technik in einem anderen Beitrag in meinem Blog über überschreiben Methoden verwenden, aber immer noch die ursprüngliche Implementierung: http://naleid.com/blog/2009/06/01/groovy-metaclass-overriding-a-methode-while-using-the-old-implementation Dies könnte durch das Interface-Problem, das Sie identifiziert haben, jedoch blockiert werden ... Wenn ja, dann das Lösung, die du hast, sieht für mich wie eine gute aus. –

+0

Dank Ted, das hat das gleiche Problem mit der Schnittstelle verhindert es zu arbeiten. Ich interessiere mich für Ihre Gedanken über Zwangstypen. Der einfache Code schlägt für 'project.files (Object ... paths)' für 'files (null)' und 'files()' fehl. Gibt es eine bessere Möglichkeit, die Typen für einen Varargs-Parameter generisch zu erzwingen? Fühlen Sie sich frei, meinen Beitrag mit Verbesserungen zu bearbeiten. – brunobowden