Sunday, June 16, 2013

RUnit and callback functions and scope

I have a function (called something() in the below example code) that takes a callback function as a parameter. To be flexible it can take the callback as a Function object, or as a character string, or as a list (which keeps the function name and the parameters together).

But I hit a problem when trying to write a unit test for something(). It works with a global function as the callback (such as mean in the below code), but not for a locally defined function (thingy in the below code):
thingy=function(x){ mean(x) }

test.something=function(){
x=1:10

something(mean,x)
something("mean",x)
something( list("mean",x) )
something(thingy,x)
something("thingy",x)    #Fails
something( list("thingy",x) )   #Fails
}
The problem appears to be that functions in a test file are all loaded in a special environment. And, when I thought about it, that makes sense, unit test functions are supposed to be small and independent, and I don't want to be defining a function at the same level as the unit tests.

I tried a few things, but here is the one that worked:
test.something=function(){ 
assign("thingy",function(x){ mean(x) }, envir=globalenv())
 
x=1:10

something(mean,x)
something("mean",x)
something( list("mean",x) )
something(thingy,x)
something("thingy",x)
something( list("thingy",x) )

remove("thingy", envir=globalenv())
}
So, we explicitly put our thingy() in the global environment, where everyone can find it and make use of it. And we define it inside the test function, no earlier. Removing it at the end is optional, but Murphy's Law states that if we don't then it will end up clashing with something. Late on a Friday afternoon, in six months time, when we've forgotten about this code.

Monday, April 8, 2013

PhantomJS: POST, auth and timeouts

I recently discovered PhantomJS, which (for me) is an alternative to Selenium, with two key differences that (again, for me) make it very useful:
  1. It is headless, meaning you don't see anything graphical. That means I can run it on a server, from the commandline, without needing X installed. It also means it causes less load.
  2. It embeds webkit, rather than attempting to interface with many browsers, and control them as a user would.
The second point allows me to POST to a URL, which is great for testing how web services work in a real browser. Selenium refuses to offer this because it is not something a user can do with a browser. (The workaround in Selenium was to make a temporary page that uses an AJAX call to POST to the URL, then does something with what is returned.)

There are two things that PhantomJS makes difficult, which I will show techniques for here. The first is that authorization is kind-of-broken. The second is timeouts for requests that never finish (e.g. an http streaming web service). But, first, the basic example, without auth or timeouts, and using GET:
var page=require('webpage').create();
var callback=function(status){
    if (status=='success'){
        console.log(page.plainText);
        }else{
        console.log('Failed to load.');
        }
    phantom.exit();
    };
var url="http://example.com/something?name=value";
page.open(url,callback);
Now here is the same code with basic auth (shown in orange), and a five second time-out (shown in red):
var page=require('webpage').create();
page.customHeaders={'Authorization': 'Basic '+btoa('username:password')};
var callback=function(status){
    if(timer)window.clearTimeout(timer);
    if (status=='success' || status=='timedout') {
        console.log(page.plainText);
        }else{
        console.log('Failed to load.');
        }
    phantom.exit();
    };
var timer=window.setTimeout(callback,5000,'timedout');
var url="http://example.com/something?name=value";
page.open(url,callback);
Don't use page.settings.userName = 'username';page.settings.password = 'password'; because it has a bug as of PhantomJS 1.9.0 (it uses two connections for GET requests and doesn't work at all for POST requests). Instead make your own basic auth header as shown here (thanks to Igor Semenko, on the PhantomJS mailing list for this trick).

For the time-out code I still call the same callback, but pass a status of "timedout" instead of "success" (so the callback could react differently, if timedout was a bad thing - here I treat them the same). So, if the URL finishes loading within 5000ms, then callback is called (by the page.open() call) with status equal to "success". If it has not finished within 5000ms then callback is called (by the javascript timer), with status equal to "timedout".

I explicitly clear the timer immediately when entering callback(). This is not really necessary, as we're about to shutdown (the phantom.exit() call) anyway. But it feels safer because otherwise callback() might be called twice (i.e. if the page loaded in exactly 5000ms); the more computation being done in callback(), especially if asynchronous, the more this might occur. (Well to be precise: that catches the case when page loads in just under 5000ms and triggers the callback before the timer does. But, if the timer gets in first, and then the page loads in just over 5000ms, and callback computation takes a while, then we may still get two calls. I think calling page.close() in callback() might prevent this, but that is untested.)

Finally, here is the same code using POST instead of GET:
var page=require('webpage').create();
page.customHeaders={'Authorization': 'Basic '+btoa('username:password')};
var callback=function(status){
    if(timer)window.clearTimeout(timer);
    if (status=='success' || status=='timedout') {
        console.log(page.plainText);
        }else{
        console.log('Failed to load.');
        }
    phantom.exit();
    };
var timer=window.setTimeout(callback,5000,'timedout');
var url="http://example.com/something";
var data="name=value";
page.open(url,'post',data,callback);
The differences are shown in red. It couldn't be easier!

Monday, February 4, 2013

Making sense of vfsStream

vfsStream is a very cool PHP library that abstracts the file system. Its primary use case is in unit tests, as a way to mock file system activity, and it integrates nicely with PHPUnit, even being mentioned in the PHPUnit manual.

Sadly the documentation is a bit lacking, so this article will try provide some middle ground behind the unexplained usage example you'll find in a few places, and the dry API docs. (In fact skip the API docs completely - the source is more understandable.)

I'm going to show this completely outside PHPUnit, as that clouds the issues. Let's start with the include we need:
   require_once 'vfsStream/vfsStream.php';

(Aside: all tests here are being done with version 0.12.0, installed using pear.)

Here is our minimal example:

   vfsStream::setup('logs');
   file_put_contents(vfsStream::url('logs/test.log'),"Hello\n");


The first line creates our filesystem.
The second line creates a file called "test.log", and puts a string inside it.

There is something I want to emphasize here, as it confused me no end. We have not created a directory called "logs". We have created a virtual file system called "logs". We have no sub-directories at all. test.log lives in the root of the filesystem.

Here is our troubleshooting tool:
   require_once 'vfsStream/visitor/vfsStreamStructureVisitor.php';
   print_r(vfsStream::inspect(new vfsStreamStructureVisitor())->getStructure());

It outputs:
    Array
    (
        [logs] => Array
            (
                [test.log] => Hello
            )
    )


(Yes, I know it still looks like logs is a directory name there too. It isn't.)

Another way to troubleshoot this is using PHP functions directly:
    $dir=dir("vfs://logs");
    while(($entry=$dir->read())!==false)echo $entry."\n";


This outputs:
    test.log

Note: you cannot use "vfs://" as a url to get the root. This is like trying to access the root of a website with "http://". You need to use "http://example.com/" and you need to use "vfs://logs" for a virtual filesystem. As I said, I found this confusing, so I prefer to rewrite my above examples as follows (this also shows the complete code):

   
    require_once 'vfsStream/vfsStream.php';
    vfsStream::setup('_virtualroot_',null,array('logs'=>array()));
    file_put_contents(vfsStream::url('_virtualroot_/logs/test.log'),"Hello\n");
    print_r(vfsStream::inspect(new vfsStreamStructureVisitor())->getStructure());
    $dir=dir("vfs://_virtualroot_");
    while(($entry=$dir->read())!==false)echo $entry."\n";

    ?>

This time we do have a directory called "logs", which is in the root of a virtual file system called "_virtualroot_". You may hate that approach for its horrible verbosity. Me? I like it.

A few random other points about vfsStream:
  • chdir("vfs://logs") does not work. The Known Issues page lists a few more.
  • vfsStream::url("logs/test.log") simply returns "vfs://logs/test.log".
  • fopen($fname,"at") fails. The "t" is not supported. You have to change your code to use "a" or "ab" (aside: "ab" is better, as "a" could work differently on different systems).
  • Calling vfsStream::setup  a second time removes the previous filesystem, even if you use a different virtual filesystem name.
  • I've not worked out how to use the git version. It may be that my above examples do not work with the very latest version. If I work it out, and that is the case, I'll post additional examples.

Friday, December 21, 2012

EC2: move a large file between Windows instances

Moving a file between linux machines is easy-peasy, just use scp. (You can be sure ssh/scp is on all your linux ec2 machines.) Windows? Sigh, Windows. To get ssh/scp on Windows you need to install cygwin, and that is a non-trivial step to take.

So, how to move a 60GB file to move from one Windows ec2 instance to another Windows ec2 instance (in a different region)? Here is what I did:
  1. Install CloudBerry Pro on the source server. (Must be the pro version: you can get a 14-day free trial; when that expires it is a $30 cost)[1]
  2. Install CloudBerry free version on the target server.
  3. Test copying over a small file, via your S3 account, to make sure it works. I'm assuming you already know how to use this type of two-pane file-copy application. (I created the bucket in the same region as the target machine: that means the upload takes longer than the download.)
  4. In CloudBerry Pro, Tools menu, then Options, then choose Compression And Encryption tab. Check "Use Compression".[2]
  5. Copy the big file. It gives no progress.
  6. When it had finished it said it was 21% done. Very confusing. And on the server it just showed as 13GB file, not a 60GB one.[3]
  7. Download to your target server, using CloudBerry free version. (yes it works fine to download large files, to download compressed files.)
  8. Rename your downloaded file with a ".gz" extension, as that is what it actually is.[3]
  9. Install 7-zip, if you don't have a program that can deal with gzip files. It tells me the file is 2GB compressed, 13GB uncompressed. Ignore that, it is just being stupid. Decompress it, and you get a 60GB file.
Phew,  hard work. If you needed to do it regularly you should install cygwin and use scp! <soap-box>Or port your applications to linux where the living is easy. Apart from the fact that running Windows machine is harder, it is also significantly cheaper to run the cloud instances.</soap-box>

[1]: I've heard, but not confirmed, that you can uninstall it from one machine, then use the same install key on a different machine. If true, that is quite a fair license, and I encourage you to support them.

[2]: As we saw, this creates more work, so uncheck after doing your big file.

[3]: I think CloudBerry Pro should have put a .gz extension on the file, when it uploaded it, to make it clear what was going on.