Mr. Cluey
: Library : FunctionBuried within the appendix of the 4Test Language Reference, hiding within the comparison to the C language, lie a few scant lines of documentation which discuss the single most useful operator in the language.
Variable References are discussed elsewhere, and are well worth examining. But the real benefit in the reference operator is the ability to use it to access functions through strings. If you can access a function through a string, you can then pass that function to another function to manipulate it in some way.
To make life easier for myself, I created a user defined type, FUNCTION, for all strings which will be referenced as functions. The type definition is in function.inc but don't bother to look - type FUNCTION is string [Note: The entire QAPlib can be downloaded - click here - KZ]
It is possible to pass the parameters which will be used by the called function. It is also possible to pass methods (functions associated with particular windows; Foo.Click() for instance). Both of these are beyond the scope of this article, but will be addressed in Advanced Function References
I have run into three distinct applications for referencing function through strings. Doubtless there are others
Before we race into our code and convert every function call to a referenced string call, it is worth taking a moment to become aware of the risks.
As you would expect, the primary risk is confusion. The reference operator is buried in the back of the manuals. You pretty much have to be told it is there (you're welcome) before you will find it. It is therefore not the type of thing you can expect the next person to come along to understand. So make certain that any time you use the reference operator that you also carefully document why you are doing that.
A further maintenance risk is that the compiler cannot catch errors in reference function calls. @("Pront") won't print anything, but it will compile quite happily - only at run time will you discover the problem. You can improve the chances of catching this problem during the compile by creating variables ( string funcPrint = "Print" ) which will catch some, though not all, of your typos.
Although calling referenced functions is generally straightforward, passing the function parameters through a function wrapper is a real pain, unless you make special efforts in the implementation of the target function.
One final note: there is no way, short of actually calling the function, to determine if the function actually exists.
A function shell encloses some target function within a block of code designed to perform some task before, or after, the function is called. There are all kinds of handy things to do within the shell; some are complicated, but most are not.
The easiest function that I can think of, which actually accomplishes something, is TestFunction (), which converts a normal function into a testcase:
FUNCTION funcFoo = "Foo" ;
Foo () { print ("xyzzy") ; }
testcase TestFunction( FUNCTION sFunc )
{
@(sFunc) () ;
}
With the fragment above, a call to Foo () simply prints the string. But TestFunction("Foo") actually calls testcaseEnter, testcaseExit, and so forth. Quite frankly, TestFunction really is not a function I would be proud to have in my testware - but it does have some use, which puts it one up on my original example.
A more practical example, which I have put into production, is BitmapLoop(). Without getting too bogged down in the detail, the idea was to capture the image of a window, call some function which would probably force the window to repaint, then compare the new image of the window to the original.
BitmapLoop ( FUNCTION func )
{
wTarget.CaptureBitmap( "c:\temp.bmp" ) ;
@(func) () ;
wTarget.CompareBitmap( "c:\temp.bmp" ) ;
}
The functions that were passed in were written such that the final image of the window should be the same as the original (for example, minimizing the window, then restoring it).
The most useful of the looping functions that I have written is PerformanceCheck(), which uses QAP to perform coarse timing of the application under test. This function is described in more detail in Perform.
Some compilers have associated with them a preprocessor. One benefit of the preprocessor is that blocks of code can be enabled or disabled before the code is actually compiled. You might, for example, designate a block in your code to be compiled only if a particular library has been included in the sources.
In partner, we can mimic this particular functionality, at least to some degree. We cannot tell whether a particular file has been included in our script - but we can try to execute a function, and trap the failure to find it, which is almost as good. The function which does this, located in function.inc, is CallFunctionIfExists ()
CallFunctionIfExists accepts a string, which it uses as the reference to a function. If the function named by the string exists, that function gets executed normally. If that function does not exist, partner raises the exception E_UNDEFINED, which is trapped. This permits a graceful exit should the function not exist.
There is one tricky bit worth looking at - we need to ensure that the exception was raised because the function we tried to call was missing - we should get an exception if the function we called attempted to call a function that didn't exist.
I found this function rather handy, especially when I first started working in a testbed designed for working with two different applications. It allowed me to use one function to set up my test environment (not necessarily a good idea, by the way) regardless of which application I was testing. The fragment looked something like this
SetupEnvironment ()
{
CallFunctionIfExists( "SetupAppFoo" ) ;
CallFunctionIfExists( "SetupAppBar" ) ;
}
The advantage here is that I didn't need to include all of Foo's libraries when I was trying to test Bar. Furthermore, I could test Foo even when I had rendered the fundamental libraries of Bar completely uncompileable.
From time to time, the way you want some function executed will depend on which test environment you are working in. For example, you might want to alter the methods that you use to report known bugs. The normal, safe way to do this is to set a parameter somewhere to select a method from a predetermined list.
But here is an alternative implementation
FUNCTION funcReportBug ;
ReportBug ( string sMsg )
{
@(funcReportBug)( sMsg) ;
}
Using this implementation, you configure funcReportBug to some string ( "Print" - for example) and Hey Presto! that function gets called every time ReportBug is called.
| Mr. Cluey : Library : Function |