我可以用 Perl 编写 DSL 吗?

发布于 2024-07-10 00:18:53 字数 634 浏览 6 评论 0 原文

我们使用 Perl 进行 GUI 测试自动化。 它非常成功。 我们为 GUI 测试编写了一种非常轻量级的 DSL 语言。 DSL 与对象模型非常相似。

例如,我们在根有一个 Application 对象。 应用程序中的每个属性表都是一个 View 对象。 页面下的每个页面称为Page对象本身。 我们从 Perl 向 GUI 应用程序发送命令,GUI 解释该命令并很好地响应该命令。 要发送命令,我们执行以下操作:

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

这不太可读。 相反,我想为应用程序、视图和页面编写 Perl DSL。 Perl 是否提供了某种 DSL 结构,我可以在其中执行以下操作?

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

其中 App 应是 Application 类的实例。 我必须在运行时获取 View2 的对象。

这样的东西怎么用呢?

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?

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(6

溺深海 2024-07-17 00:18:53

您几乎可以用 Perl 做任何事情。 但是你必须做一些奇怪的事情来让 Perl 使用非 Perl 的语法来执行。

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.)

遗失的美好 2024-07-17 00:18:53

我建议您停止尝试做奇怪的“DSL”事情,而只编写 Perl 类来处理您想要管理的对象。 我建议您考虑使用新的 Moose Perl 对象系统来实现此目的,尽管传统的 Perl OO 就可以了。 深入研究 OO 教程的 Perl 文档; 他们都是伟大的。

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.

断桥再见 2024-07-17 00:18:53

perl5 中的方法调用使用 -> 而不是 .,因此它看起来像 App->View2->Page2->Activate()< /code> 或 $App->View2->Page2->Active() 除非您做了一些非常有趣的事情(例如,源过滤器)。 假设没问题,您可以使用普通的 Perl OO 内容。

现在,您需要的下一部分是在运行时创建方法。 这实际上相当简单:

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
}

或者,如果您只想在调用方法时创建方法,那么 AUTOLOAD 就是这样做的。 您还可以滥用自动加载来使所有方法调用成功(但要注意具有特殊含义的方法,例如 DESTROY)。

这将为您提供语法。 让您的对象生成一个字符串传递给 send_command 应该不会那么困难。

另外,我对它不太熟悉,但你可能想看看 Moose 。 它可能有更简单的方法来实现这一点。

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.

老旧海报 2024-07-17 00:18:53

DSL 源过滤器

这是另一种尝试。 Skiphoppy 有一点道理,但再看一遍,我注意到(到目前为止)你并没有问太多那么复杂的问题。 您只想获取每个命令并告诉远程服务器执行该操作。 perl 不需要理解这些命令,而是服务器。

因此,我删除了一些有关源过滤器的警告,并决定向您展示
如何写一个简单的。 再说一次,你所做的事情并不那么复杂,我下面的“过滤”非常简单。

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;

您需要将其保存在 Perl 可以找到的 RemoteAppScript.pm 中。 (如果您需要知道在哪里,请尝试 perl -MData::Dumper -e 'print Dumper( \@INC ), "\n"' 。)

然后您可以创建一个包含以下内容的“perl”文件: this:

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

但是,

没有真正的原因表明您无法读取包含服务器命令的文件。 这会抛出 FILTER 调用。 你的脚本文件中会有

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

这样的内容,你的 perl 文件看起来更像这样:

#!/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 );
}

并像这样调用它:

perl run_remote_script.pl remote_app_script.ras

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
蓝眸 2024-07-17 00:18:53

http://search.cpan.org/dist/Devel-Declare/ 是现代的源过滤器的替代方案,它可以直接集成到 perl 解析器中,值得一看。

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.

挽梦忆笙歌 2024-07-17 00:18:53

覆盖 '.' 或使用 -> 语法的替代方法可能是使用包语法 (::),即创建 App::View2 和 App::View2 等包::Page2 当 View2 / Page 2 创建时,将 AUTOLOAD 子添加到委托给 App::View::Page 或 App::View 方法的包中,如下所示:

在您的 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;

中你的脚本:

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();

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();
~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文