tags:

views:

800

answers:

6

We use Perl for GUI test automation. It has been very successful. We have written a very lightweight DSL kind of language for GUI testing. The DSL is very similar to a object model.

For example, we have an Application object at the root. Each property sheet in the application is a View object. Each page under the page is called Page object itself. From Perl we send commands to a GUI application and the GUI interpret the command and respond to the command nicely. To send a command we do the following:

socket_object->send_command("App.View2.Page2.Activate()")
socket_object->send_command("App.View1.Page3.OKBtn.Click()")

This is not very readable. Instead, I want to write a Perl DSL for App, View and Page. Does Perl provide some sort of DSL structure where I can do the following?

App.View2.Page2.Activate();
App.View1.Page2.Click();

Where App shall be an instance of the Application class. I have to get the object of View2 at run time.

How to use such a things?

+4  A: 

Method calls in perl5 use -> not ., so it'll look like App->View2->Page2->Activate() or $App->View2->Page2->Active() unless you do something really interesting (e.g., a source filter). Assuming that's OK, you can use normal Perl OO stuff.

Now, the next part of what you need is to create the methods at runtime. This is actually fairly simple:

sub _new_view {
    my ($view, $view_num);

    # ...
    # ... (code to create $view object)
    # ...

    my $sym = "App::View$view_num";
    *$sym = sub { return $view }; # can also use Symbol package
}

Alternatively, if you want to create the methods only when they're called, thats what AUTOLOAD does. You can also abuse autoload to make all method calls succeed (though watch out for ones with special meanings, like DESTROY).

This will get you the syntax. Having your objects generate a string to pass to send_command should not be that difficult.

Also, I'm not too familiar with it, but you may want to check out Moose. It may have easier ways to accomplish this.

derobert
+14  A: 

You can do almost anything in Perl. But you have to do some strange stuff to get Perl to perform with syntax that is just not Perl.

  • To handle exactly what you have there, you would have to a lot of advanced tricks, which are by definition not that maintainable. You would have to:

    • overload the concatenation operator '.' (requires a blessed reference)
    • turn strictures off or create an AUTOLOAD subs to allow for those bare words - of course, you could write subs for all of the words you wanted to use (or use the barewords module).
    • possibly, create multiple packages, with multiple AUTOLOADs
  • Another way is source filters, I can probably pick up a downvote just for mentioning this capability. So I wouldn't exactly recommend this approach for people who are asking for help. But it's out there. Source filters (and I've done my share) are just one of those areas where you can think you're too clever for your own good.

    Still, if you are interested in Perl as a DSL "host" language, then source filters aren't exactly off limits. However, limiting this to just what you show that you want to do, Perl6::Attributes will probably do most of what you would need right off the shelf. It would take the . and translate them into the "->" that Perl would understand. But you can still take a look at source filters to understand what's going on behind the scenes.

    I also don't want to leave this topic without suggesting that a lot of the frustration you could have generating your own source filter (which I advise NOT to do) is eased by using Damian Conway's Filter::Simple.

  • The simplest thing is to forgo the '.' operator and just instead expect Perl-looking code.

    App->View2->Page2->Activate(); 
    App->View1->Page2->Click();
    

    App would be either a package or a sub. Either defined in the current package or imported which returns an object blessed into a package with a View2 sub (possibly an AUTOLOAD sub) which returns either the name of a package or a reference blessed into a package, that understands Page2, and then finally the return from that would understand Activate or Click. (See the OO tutorial, if you need.)

Axeman
+5  A: 

I recommend you quit trying to do freaky "DSL" stuff and just write Perl classes to handle the objects you want to manage. I recommend you look into using the new Moose Perl object system for this, although traditional Perl OO would be just fine. Dig through the Perl documentation for the OO tutorials; they are great.

skiphoppy
How about insideout?
Krish
I don't know anything about insideout, so I can't speculate on what value it might add for you. What's wrong with just programming traditional OO classes to handle these?
skiphoppy
+4  A: 

DSL Source Filter

Here's another attempt. skiphoppy has a point, but on second look, I noticed that (so far) you weren't asking much that was that complex. You just want to take each command and tell the remote server to do it. It's not perl that has to understand the commands, it's the server.

So, I remove some of my warnings about source filters, and decided to show you how a simple one can be written. Again, what you're doing is not that complex, and my "filtering" below is quite easy.

package RemoteAppScript;
use Filter::Simple;    # The basis of many a sane source filter
use Smart::Comments;   # treat yourself and install this if you don't have 
                       # it... or just comment it out.

# Simple test sub
sub send_command { 
    my $cmd = shift;
    print qq(Command "$cmd" sent.\n);
    return;
}

# The list of commands
my @script_list;

# The interface to Filter::Simple's method of source filters.
FILTER { 
    # Save $_, because Filter::Simple doesn't like you reading more than once.
    my $mod = $_;

    # v-- Here a Smart::Comment.
    ### $mod

    # Allow for whole-line perl style comments in the script
    $mod =~ s/^\s*#.*$//m;

    # 1. Break the package up into commands by split
    # 2. Trim the strings, if needed
    # 3. lose the entries that are just blank strings.
    @script_list 
        = grep { length } 
          map  { s/^\s+|\s+$//g; $_ } 
          split /;/, $mod
        ;
    ### @script_list

    # Replace the whole script with a command to run the steps.
    $_ = __PACKAGE__ . '::run_script();';
    # PBP.
    return;
};

# Here is the sub that performs each action.
sub run_script { 
    ### @script_list
    foreach my $command ( @script_list ) {
        #send_command( $command );
        socket_object->send_command( $command );
    }
}

1;

You would need to save this in RemoteAppScript.pm somewhere where your perl can find it. ( try perl -MData::Dumper -e 'print Dumper( \@INC ), "\n"' if you need to know where.)

Then you can create a "perl" file that has this:

use RemoteAppScript;
App.View2.Page2.Activate();
App.View1.Page2.Click();

However

There no real reason that you can't read a file that holds server commands. That would throw out the FILTER call. You would have

App.View2.Page2.Activate();
App.View1.Page2.Click();

in your script file, and your perl file would look more like this:

#!/bin/perl -w 

my $script = do { 
    local $/;
    <ARGV>;
};

$script =~ s/^\s*#.*$//m;

foreach my $command ( 
    grep { length() } map  { s/^\s+|\s+$//g; $_ } split /;/, $script 
) { 
    socket_object->send_command( $command );
}

And call it like so:

perl run_remote_script.pl remote_app_script.ras
Axeman
+1 for the second solution. A simple format is actually easier to parse than to make readable in Perl.
Vincent Robert
A: 

http://search.cpan.org/dist/Devel-Declare/ is modern alternative to source filters which works at integrating directly into perl parser, and is worth a look.

dpavlin
Yeah, all the cool kids write their source filters with Devel::Declare now
MkV
It's a very different way to using regexes, so it might fit different needs.
dpavlin
A: 

An alternative to overriding '.' or using -> syntax might be using package syntax (::), i.e. creating packages like App::View2 and App::View2::Page2 when View2 / Page 2 get created, adding an AUTOLOAD sub to the package which delegates to an App::View::Page or App::View method, something like this:

In your App/DSL.pm:

package App::DSL;
use strict; 
use warnings;
# use to avoid *{"App::View::$view::method"} = \&sub and friends
use Package::Stash;

sub new_view(%);
our %views;

# use App::DSL (View1 => {attr1 => 'foo', attr2 => 'bar'}); 
sub import {
    my $class = shift;
    my %new_views = @_ or die 'No view specified';

    foreach my $view (keys %new_views) {
            my $stash = Package::Stash->new("App::View::$view");
        # In our AUTOLOAD we create a closure over the right
        # App::View object and call the right method on it
        # for this example I just used _api_\L$method as the
        # internal method name (Activate => _api_activate)
        $stash->add_package_symbol('&AUTOLOAD' =>  
            sub {  
                our $AUTOLOAD;
                my ($method) = 
                   $AUTOLOAD =~ m{App::View::\Q$view\E::(.*)};
                my $api_method = "_api_\L$method";
                die "Invalid method $method on App::View::$view"
                   unless my $view_sub = App::View->can($api_method);
                my $view_obj = $views{$view}
                    or die "Invalid View $view";
                my $sub = sub {
                        $view_obj->$view_sub();
                };
                     # add the function to the package, so that AUTOLOAD
                     # won't need to be called for this method again
                $stash->add_package_symbol("\&$method" => $sub);
                goto $sub;
            });
        $views{$view} = bless $new_views{$view}, 'App::View';
    }
}

package App::View;

# API Method App::View::ViewName::Activate;
sub _api_activate {
    my $self = shift;
    # do something with $self here, which is the view 
    # object created by App::DSL
    warn $self->{attr1};
}

1;

and in your script:

use strict;
use warnings;
# Create App::View::View1 and App::View::View2
use App::DSL (View1 => {attr1 => 'hello'}, View2 => {attr1 => 'bye'});
App::View::View1::Activate();
App::View::View2::Activate();
App::View::View1::Activate();
MkV