blow up after failing to start the selenium server - no point continuing
[scpubgit/Test-Harness-Selenium.git] / lib / Test / Harness / Selenium.pm
1 package Test::Harness::Selenium;
2 use strictures 1;
3
4 use File::Find::Rule;
5 use Socialtext::WikiFixture::Selenese;
6 use HTML::TableExtract;
7 use IO::All;
8 use Alien::SeleniumRC;
9 use LWP::Simple;
10 use Child;
11
12 our $VERSION = '0.01';
13
14 use Test::More;
15 BEGIN {
16   package Test::Builder;
17
18   use Class::Method::Modifiers;
19   use ExtUtils::MakeMaker qw(prompt);
20
21   if (!$ENV{AUTOMATED_TESTING}) {
22     around ok => sub {
23       my ($orig, $self) = (shift, shift);
24       my $res = $self->$orig(@_);
25       unless ($res) {
26         if ('y' eq prompt "Well that didn't work, did it. Bail out?", 'y') {
27           exit 255;
28         }
29       }
30       return $res;
31     };
32   }
33 }
34
35 sub new {
36   my $class = shift;
37   my %args = @_;
38   my $selrc = ($args{selenium_rc} ||={});
39   $selrc->{$_} = $ENV{"SELENIUM_RC_${\uc $_}"}
40     for grep exists $ENV{"SELENIUM_RC_${\uc $_}"},
41       qw(host port start start_xvnc xvnc_display);
42   $selrc->{xvnc_display} ||= '0';
43   $selrc->{host} ||= 'localhost';
44   $selrc->{port} ||= 4444;
45   $args{browser} ||= '*firefox';
46   my $self = \%args;
47   bless $self, $class;
48 }
49
50 sub start_selenium_server {
51   my($self) = @_;
52   my $selrc = $self->{selenium_rc};
53   if($selrc->{start}) {
54     my ($host, $display, $port) = @{$selrc}{qw(host xvnc_display port)};
55     my @do_ssh = $host eq 'localhost' ? () : ('ssh', $host);
56     if ($selrc->{start_xvnc}) {
57       $selrc->{xvnc_server_proc} = Child->new(
58           sub {
59             exec(
60               @do_ssh,
61               'vncserver', ":${display}",
62             );
63           }
64         )->start;
65       $selrc->{xvnc_started} = 1;
66       sleep 3;
67     }
68     $selrc->{selenium_server_proc} = Child->new(
69       sub {
70         exec(
71           @do_ssh,
72           'env', "DISPLAY=:${display}", 'selenium-rc', '-port', $port
73         )
74       }
75     )->start;
76     sleep 1;
77   }
78   my $tries = 0;
79   while($tries < 5) {
80     eval {
81       # if we don't create the ::Selenium object ourselves, then
82       # wikifixture shuts the session down after the first test table
83       # is run, at which point KABOOM when you try and run a second one.
84       $self->{src} = Socialtext::WikiFixture::Selenese->new(
85         selenium => Test::WWW::Selenium->new(
86           host => $self->{selenium_rc}{host},
87           port => $self->{selenium_rc}{port},
88           browser => $self->{browser},
89           browser_url => $self->{app_base},
90         )
91       );
92     };
93     $tries++;
94     if(!defined $self->{src}) {
95       sleep 10;
96     }
97     else {
98       last;
99     }
100   }
101   if($tries == 5) {
102     diag "timed out waiting for selenium server to start at
103     http://$self->{selenium_rc}{host}:$self->{selenium_rc}{port}" if $tries == 5;
104     $self->done;
105     die;
106   }
107 }
108
109 sub stop_selenium_server {
110   my($self) = @_;
111   if (my $proc = delete $self->{selenium_rc}{selenium_server_proc}) {
112     my $url = sprintf
113       "http://%s:%s/selenium-server/driver/?cmd=shutDownSeleniumServer",
114       $self->{selenium_rc}{host}, $self->{selenium_rc}{port};
115     eval { get($url); }; # will fail if it never started
116     delete $self->{src};
117     $proc->kill("KILL");
118   }
119   if (delete $self->{selenium_rc}{xvnc_started}) {
120     my $host = $self->{selenium_rc}{host};
121     my @do_ssh = $host eq 'localhost' ? () : ('ssh', $host);
122     Child->new(sub {
123       exec(@do_ssh, 'vncserver', '-kill',
124         ":$self->{selenium_rc}{xvnc_display}");
125     })->start->wait;
126   }
127 }
128
129 sub start_app_server {
130   my($self) = @_;
131   my $child = Child->new(sub { exec($self->{app_server_cmd}) } );
132   $self->{app_server_proc} = $child->start;
133 }
134
135 sub stop_app_server {
136   my($self) = @_;
137   $self->{app_server_proc}->kill("KILL");
138 }
139
140 sub test_directory {
141   my ($self, $dir) = @_;
142   if(!exists $self->{app_server_proc}) {
143     $self->start_app_server;
144   }
145   if($self->{selenium_rc}{start} && !$self->{selenium_rc}{selenium_server_proc}) {
146     $self->start_selenium_server;
147   }
148   my @tests = File::Find::Rule->file()->name('*.html')->in($dir);
149   for my $test (@tests) {
150     $self->run_tests_for($test);
151   }
152 }
153
154 sub run_tests_for {
155   my ($self, $html_file) = @_;
156   my $rows = $self->get_rows_for($html_file);
157   $self->{src}->run_test_table($rows);
158 }
159
160 my $te = HTML::TableExtract->new;
161 sub get_rows_for {
162   my ($self, $html_file) = @_;
163   my $html = io($html_file)->all;
164   $te->parse($html);
165   my $table = ($te->tables)[0];
166   my @rows = map {
167     [ map { (!defined $_ or $_ eq "\240") ? () : $_ } @$_ ]
168   } grep { defined $_->[1] } $table->rows;
169   return \@rows;
170 }
171
172 sub done {
173   my($self) = @_;
174   if(exists $self->{selenium_rc}{xvnc_server_proc} and 
175     exists $self->{selenium_rc}{selenium_server_proc}) {
176     $self->stop_selenium_server;
177   }
178   $self->stop_app_server;
179 }
180
181 1;
182
183 __END__
184
185 =head1 NAME
186
187 Test::Harness::Selenium - Test your app with Selenium
188
189 =head1 SYNOPSIS
190
191   # t/selenium.t
192   my $ths = Test::Harness::Selenium->new(
193     selenium_rc => {
194       host => 'selenium_xvnc_server',
195       port => 12345,
196       start => 1, # start xvnc and selenium RC via ssh(1)
197     },
198     browser => '*firefox',
199     # app_base is relative from the machine running selenium_rc
200     app_base => 'http://10.0.0.5:3000/',
201     app_start_cmd => 'script/myapp_server.pl -p 3000',
202   );
203   # HTML tables as emitted by the Selenium IDE
204   $ths->test_directory('t/selenium_corpus');
205
206
207   # or, if you've got a selenium server already running (say, on a designer's
208   # Win32 box)
209   my $ths = Test::Harness::Selenium->new(
210     selenium_rc=> {
211       host => 'designers_machine',
212       port => 54321,
213       start => 0, # they've already got the RC running
214     },
215     browser => '*iexplore', # can't live with it, can't live without it
216     app_base => 'http://10.0.0.5:3000/',
217     app_start_cmd => 'script/myapp_server.pl -p 3000',
218   );
219   # otherwise the same
220   $ths->test_directory('t/selenium_corpus');
221
222 =head1 DESCRIPTION
223
224 C<Test::Harness::Selenium> provides an abstracted way of doing Web app testing
225 using the Selenium framework. It will connect to a running Selenium RC server,
226 or start one using ssh(1) to connect to a machine and then launching the RC
227 server and an xvnc(1) instance in which to run the given browser. After the
228 connection is established, Test::Harness::Selenium will read the specified HTML
229 files from disk and massage them into a format that
230 L<Socialtext::WikiFixture::Selenese> can parse and send to the Selenium RC
231 server. The RC server will then script the browser to do the actions described
232 in the HTML files. These actions return results, which Test::Harness::Selenium
233 then interprets as passing or failing as appropriate.
234
235 =head1 METHODS
236
237 =head2 new
238
239 =over 4
240
241 =item arguments: %attrs
242
243 =item Return value: new Test::Harness::Selenium object
244
245 =back
246
247 Constructor. Accepts a list of key/value pairs according to the following:
248
249 =over 4
250
251 =item selenium_rc
252
253 Hashref. Accepts the keys host, port, start. host and port describe the server
254 on which to start/find the Selenium RC server and possibly the xvnc server.
255 start is a Boolean indicating whether to start the Selenium RC server and xvnc
256 server.
257
258 =item browser
259
260 Scalar. The browser to use for testing the app. Must be in a form that Selenium
261 RC understands (e.g. '*firefox'); see the Selenium docs for more info.
262
263 =item app_base
264
265 Scalar. The URL, relative to the machine running Selenium RC, for the base of
266 the app. All requests made to the app are relative to this URL.
267
268 =item app_start_cmd
269
270 Scalar. This command will be run by start_app_server to run the app server to
271 test.
272
273 =back
274
275 =head2 test_directory
276
277 =item arguments: $dir
278
279 =item Return value: None
280
281 Object method. test_directory will use L<File::Find::Rule> to find all C<< .html >>
282 files in the given directory, and then formats massages them into data
283 structures that L<Socialtext::WikiFixture::Selenese> can send to the Selenium RC
284 server. test_directory will then output appropriate TAP according to whether the
285 tests and checks passed or failed, respectively.
286
287 =head2 run_tests_for
288
289 =item arguments: $html_file
290
291 =item Return value: None
292
293 run_tests_for is called by test_directory for each C<< .html >> file it finds in
294 the given directory.
295
296 =head2 start_app_server, start_app_server
297
298 =item Arguments: None
299
300 =item Return value: None
301
302 Start and stop the app server using the command given to the constructor.
303
304 =head2 start_selenium_server, start_selenium_server
305
306 =item Arguments: None
307
308 =item Return value: None
309
310 Start and stop the selenium / xvnc servers using the params given to the
311 constructor.
312
313 =head1 ENVIRONMENT
314
315 =head2 SELENIUM_RC_HOST, SELENIUM_RC_PORT, SELENIUM_RC_START
316
317 These values override the matching values in the selenium_rc hashref passed to
318 new.
319
320 =head1 AUTHOR
321
322 Chris Nehren <c.nehren/ths@shadowcat.co.uk>, Matt S. Trout <mst@shadowcat.co.uk>
323
324 =head1 CONTRIBUTORS
325
326 No one, yet. Patches most welcome!
327
328 =head1 COPYRIGHT
329
330 Copyright (c) 2011 the Test::Harness::Selenium "AUTHOR" and
331 "CONTRIBUTORS" as listed above.
332
333 =head1 LICENSE
334
335 This library is free software and may be distributed under the same terms as
336 perl itself.