Thursday 11 July 2013

Weird issue with MockBox and interfaces: issue identified

G'day:
A coupla days back I wrote a vague article "Weird issue with Mockbox and interfaces", which detailed some weirdness I was seeing with using Mockbox to mock methods in CFCs which implement an interface.  The short version is that on the first use of a method mocked this way, I was getting this error:

coldfusion.runtime.InterfaceRuntimeExceptions$ArgumentsMistmatchException: Function argument mismatch.[etc]

However if one just re-ran the test, the problem went away. In fact it was only the first usage of the mocked method after ColdFusion was started that caused the problem. Subsequent runs: all good. Restart CF: problem. This is on ColdFusion 9 & 10, but not Railo.



Initially this looked like a ColdFusion problem, given it seemed to be something to do with the first-run of the method, and that it didn't happen on Railo.

However I did quickly see a shortfall in how MockBox mocks methods, and having fixed it: the problem goes away. However the fix is such that I don't understand how the way MockBox mocks method in a situation where there are interface concerns ever worked, which is a curious thing.

I've distilled all this down to two things. One I know the answer for, the other I do not yet.

MockBox

When MockBox mocks a method, it does not mock its argument requirements. This is a bug, because this is important information relating to how the method works, and if the method is under contract of an interface, it breaks that contract. Consider this situation:

<!--- I.cfc --->
<cfinterface>

    <cffunction name="g" output="false" returntype="string">
        <cfargument name="s" type="string" required="true">
    </cffunction>

</cfinterface>

<!--- Impl.cfc --->
<cfcomponent implements="I">
    <cffunction name="g" output="false" returntype="string">
        <cfargument name="s" type="string" required="true">
        <cfreturn ucase(s)>
    </cffunction>
</cfcomponent>

If I use MockBox to mock g(), I get this code generated:

<cfset this[ "g" ] = variables[ "g" ]>
<cffunction name="g" access="public" output="false" returntype="string">
    <cfset var results = this._mockResults>
    <cfset var resultsKey = "g">
    <cfset var resultsCounter = 0>
    <cfset var internalCounter = 0>
    <cfset var resultsLen = 0>
    <cfset var argsHashKey = resultsKey & "|" & this.mockBox.normalizeArguments(arguments)>

    <!--- If Method & argument Hash Results, switch the results struct --->
    <cfif structKeyExists(this._mockArgResults,argsHashKey)>
        <cfset results = this._mockArgResults>
        <cfset resultsKey = argsHashKey>
    </cfif>

    <!--- Get the statemachine counter --->
    <cfset resultsLen = arrayLen(results[resultsKey])>

    <!--- Log the Method Call --->
    <cfset this._mockMethodCallCounters[listFirst(resultsKey,"|")] = this._mockMethodCallCounters[listFirst(resultsKey,"|")] + 1>

    <!--- Get the CallCounter Reference --->
    <cfset internalCounter = this._mockMethodCallCounters[listFirst(resultsKey,"|")]>
    <cfset arrayAppend(this._mockCallLoggers["g"], arguments)>

    <cfif resultsLen neq 0>
        <cfif internalCounter gt resultsLen>
            <cfset resultsCounter = internalCounter - ( resultsLen*fix( (internalCounter-1)/resultsLen ) )>
            <cfreturn results[resultsKey][resultsCounter]>
        <cfelse>
            <cfreturn results[resultsKey][internalCounter]>
        </cfif>
    </cfif>
</cffunction>

Almost all of that is the inner workings of how MockBox does stuff and is irrelevant to anything I wish to observe, the important bit is the method signature that this method implies:

public string function g()

However the interface contract stipulates this:

public string function g(required string s)

So - like I said - I have no idea how this ever works. It also - as we found yesterday when a test was passing when it shouldn't've - reduces the stability of tests. I can have code which doesn't pass s to g(), and it could still pass the test, whereas it should error.

The reason it "works" at all is down to how ColdFusion loads / reads / handles component metadata (I'll get to that).

The fix for this is easy. MockBox's mock generation method is as follows (extract):

    <!--- generate --->
    <cffunction name="generate" output="false" access="public" returntype="string" hint="Generate a mock method and return the generated path">
        <!--- ************************************************************* --->
        <cfargument name="method"             type="string"     required="true" hint="The method you want to mock or spy on"/>
        <cfargument name="returns"             type="any"         required="false" hint="The results it must return, if not passed it returns void or you will have to do the mockResults() chain"/>
        <cfargument name="preserveReturnType" type="boolean" required="true" default="true" hint="If false, the mock will make the returntype of the method equal to ANY"/>
        <cfargument name="throwException" type="boolean"     required="false" default="false" hint="If you want the method call to throw an exception"/>
        <cfargument name="throwType"       type="string"     required="false" default="" hint="The type of the exception to throw"/>
        <cfargument name="throwDetail"       type="string"     required="false" default="" hint="The detail of the exception to throw"/>
        <cfargument name="throwMessage"      type="string"     required="false" default="" hint="The message of the exception to throw"/>
        <cfargument name="metadata"       type="any"         required="true" default="" hint="The function metadata"/>
        <cfargument name="targetObject"      type="any"         required="true" hint="The target object to mix in"/>
        <cfargument name="callLogging"       type="boolean"     required="false" default="false" hint="Will add the machinery to also log the incoming arguments to each subsequent calls to this method"/>
        <!--- ************************************************************* --->
        <cfscript>
            var udfOut = CreateObject("java","java.lang.StringBuffer").init('');
            var genPath = ExpandPath( instance.mockBox.getGenerationPath() );
            var tmpFile = createUUID() & ".cfm";
            var fncMD = arguments.metadata;

            // Create Method Signature
            udfOut.append('
            <cfset this[ "#arguments.method#" ] = variables[ "#arguments.method#" ]>
            <cffunction name="#arguments.method#" access="#fncMD.access#" output="#fncMD.output#" returntype="#fncMD.returntype#">
            <cfset var results = this._mockResults>
            <cfset var resultsKey = "#arguments.method#">
            <cfset var resultsCounter = 0>
            <cfset var internalCounter = 0>
            <cfset var resultsLen = 0>
            <cfset var argsHashKey = resultsKey & "|" & this.mockBox.normalizeArguments(arguments)>

            <!--- If Method & argument Hash Results, switch the results struct --->
            <cfif structKeyExists(this._mockArgResults,argsHashKey)>
                <cfset results = this._mockArgResults>
                <cfset resultsKey = argsHashKey>
            </cfif>

            <!--- Get the statemachine counter --->
            <cfset resultsLen = arrayLen(results[resultsKey])>

            <!--- Log the Method Call --->
            <cfset this._mockMethodCallCounters[listFirst(resultsKey,"|")] = this._mockMethodCallCounters[listFirst(resultsKey,"|")] + 1>

            <!--- Get the CallCounter Reference --->
            <cfset internalCounter = this._mockMethodCallCounters[listFirst(resultsKey,"|")]>
            ');
            
All they need to do is to add some code in to generate the <cfargument> tags too:

// Create Method Signature
udfOut.append('
<cfset this[ "#arguments.method#" ] = variables[ "#arguments.method#" ]>
<cffunction name="#arguments.method#" access="#fncMD.access#" output="#fncMD.output#" returntype="#fncMD.returntype#">
');
var args = "";
for (var i=1; i <= arrayLen(fncMD.parameters); i++){
    var arg = fncMD.parameters[i];
    args &= '<cfargument name="#arg.name#"';

    if (structKeyExists(arg, "required") && arg.required){
        args &= ' required="true"';
    }
    if (structKeyExists(arg, "type")){
        args &= ' type="#arg.type#"';
    }
    args &= ">";
}
udfOut.append(args);


udfOut.append('
<cfset var results = this._mockResults>
<cfset var resultsKey = "#arguments.method#">
<cfset var resultsCounter = 0>
<cfset var internalCounter = 0>
<cfset var resultsLen = 0>
<cfset var argsHashKey = resultsKey & "|" & this.mockBox.normalizeArguments(arguments)>

<!--- If Method & argument Hash Results, switch the results struct --->
<cfif structKeyExists(this._mockArgResults,argsHashKey)>
    <cfset results = this._mockArgResults>
    <cfset resultsKey = argsHashKey>
</cfif>

<!--- Get the statemachine counter --->
<cfset resultsLen = arrayLen(results[resultsKey])>

<!--- Log the Method Call --->
<cfset this._mockMethodCallCounters[listFirst(resultsKey,"|")] = this._mockMethodCallCounters[listFirst(resultsKey,"|")] + 1>

<!--- Get the CallCounter Reference --->
<cfset internalCounter = this._mockMethodCallCounters[listFirst(resultsKey,"|")]>
');
// etc

Then the method signature gets written out correct. This solves the MockBox problem (it actually identifies another one, but I'll get to that too, I've not finished investigating it yet...)

ColdFusion

ColdFusion is still doing something weird here, because this issue only occurs on ColdFusion. ColdFusion is doing something dumb, and Railo is doing it correctly. I think it's tied down to how ColdFusion loads a component's metadata only once during the life of the CF instance, although not quite that given it only seems to notice something is amiss the first time it looks to see if the object fulfills the Interface contract (no), but subsequently doesn't seem to mind that it doesn't. So in effect the error message is correct here, and that ColdFusion then stops reporting the error is actually wrong. This is erratic and inappropriate behaviour. If CF wasn't so bloody erratic, I would have solved this a lot faster.



I'll raise this with the MockBox chaps (again, Brad, consider yourself advised, but I'll raise a bug in Jira shortly, and cross-reference it here: MOCKBOX-7), and continue my investigations on the ColdFusion side of things, as well as this other MockBox glitch. I've already found some more interesting behaviour with ColdFusion's interface implementation. I'll try to write that lot up tomorrow. But until then...

--
Scheherazade