blib/lib/HTTP/WebTest/XMLParser.pm | |||
---|---|---|---|
Criterion | Covered | Total | % |
statement | 19 | 32 | 59.3 |
branch | n/a | ||
condition | n/a | ||
subroutine | 7 | 9 | 77.7 |
pod | n/a | ||
total | 26 | 41 | 63.4 |
line | stmt | bran | cond | sub | pod | time | code |
---|---|---|---|---|---|---|---|
1 | package HTTP::WebTest::XMLParser; | ||||||
2 | 3 | 3 | 9913 | use strict; | |||
3 | 7 | ||||||
3 | 90 | ||||||
3 | 3 | 3 | 2463 | use XML::SAX; | |||
3 | 28965 | ||||||
3 | 147 | ||||||
4 | |||||||
5 | 3 | 3 | 25 | use vars qw($VERSION); | |||
3 | 9 | ||||||
3 | 648 | ||||||
6 | |||||||
7 | $VERSION = '1.00'; | ||||||
8 | |||||||
9 | my $webtest_definition_version = '1.0'; # NOTE: file lexical scope | ||||||
10 | |||||||
11 | =head1 NAME | ||||||
12 | |||||||
13 | HTTP::WebTest::XMLParser - Parse wtscript in XML representation. | ||||||
14 | |||||||
15 | =head1 SYNOPSIS | ||||||
16 | |||||||
17 | use HTTP::WebTest::XMLParser; | ||||||
18 | my ($tests, $opts) = HTTP::WebTest::XMLParser->parse($xmldata); | ||||||
19 | |||||||
20 | use HTTP::WebTest; | ||||||
21 | my $wt = new HTTP::WebTest; | ||||||
22 | $wt->run_tests($tests, $opts); | ||||||
23 | |||||||
24 | HTTP::WebTest::XMLParser->as_xml($tests, $opts, { nocode => 1 }); | ||||||
25 | |||||||
26 | =head1 DESCRIPTION | ||||||
27 | |||||||
28 | Parses a wtscript file in XML format and converts it to a set of test objects. | ||||||
29 | |||||||
30 | =head2 VERSION | ||||||
31 | |||||||
32 | $Revision: $ | ||||||
33 | |||||||
34 | =head1 XML SYNTAX | ||||||
35 | |||||||
36 | The xml format follows wtscript closely, with the following rules: | ||||||
37 | |||||||
38 | - the root element is |
||||||
39 | - global paramters are in a |
||||||
40 | - test definitions are in |
||||||
41 | - a list is represented by a |
||||||
42 | - a scalar param. is represented by a element | ||||||
43 | - a code segment is represented by a element |
||||||
44 | - named parameters are named throug a 'name' attribute | ||||||
45 | |||||||
46 | The DTD is available in 'scripts/webtest.dtd' from the distribition. | ||||||
47 | For examples see the test definitions in t/*xml from the distribution. | ||||||
48 | |||||||
49 | A conversion script from wtscript to XML is available in | ||||||
50 | 'scripts/testconversion' from the distribution. This script | ||||||
51 | also converts XML definitions from earlier alpha versions of | ||||||
52 | this module. | ||||||
53 | |||||||
54 | =head2 Example | ||||||
55 | |||||||
56 | This example is the equivalent of the same example for HTTP::WebTest | ||||||
57 | |||||||
58 | |||||||
59 | The definition of tests and params from the original example: | ||||||
60 | |||||||
61 | my $tests = [ | ||||||
62 | { test_name => 'Yahoo home page', | ||||||
63 | url => 'http://www.yahoo.com', | ||||||
64 | text_require => [ 'Quotations... ' ], |
||||||
65 | min_bytes => 13000, | ||||||
66 | max_bytes => 99000, | ||||||
67 | } | ||||||
68 | ]; | ||||||
69 | my $params = { mail_server => 'mailhost.mycompany.com', | ||||||
70 | mail_addresses => [ 'tester@mycompany.com' ], | ||||||
71 | mail => 'all', | ||||||
72 | ignore_case => 'yes', | ||||||
73 | }; | ||||||
74 | |||||||
75 | This Perl script tests Yahoo home page and sends full test | ||||||
76 | report to "tester@mycompany.com". | ||||||
77 | |||||||
78 | use HTTP::WebTest; | ||||||
79 | use HTTP::WebTest::XMLParser; | ||||||
80 | |||||||
81 | my $XML = <<"EOXML"; | ||||||
82 | |
||||||
83 | |
||||||
84 | yes | ||||||
85 | |
||||||
86 | tester@mycompany.com | ||||||
87 | |||||||
88 | mailhost.mycompany.com | ||||||
89 | all | ||||||
90 | |||||||
91 | |
||||||
92 | 13000 | ||||||
93 | 99000 | ||||||
94 | http://www.yahoo.com | ||||||
95 | Yahoo home page | ||||||
96 | |
||||||
97 | Quotations... ]]> |
||||||
98 | |||||||
99 | |||||||
100 | |||||||
101 | EOXML | ||||||
102 | |||||||
103 | my ($tests, $params) = HTTP::WebTest::XMLParser->parse($XML); | ||||||
104 | |||||||
105 | my $webtest = new HTTP::WebTest; | ||||||
106 | $webtest->run_tests($tests, $params); | ||||||
107 | |||||||
108 | =head1 CLASS METHODS | ||||||
109 | |||||||
110 | =head2 parse ($xmldata) | ||||||
111 | |||||||
112 | Parses wtscript in XML format passed in C<$xmldata> as string. | ||||||
113 | |||||||
114 | =head3 Returns | ||||||
115 | |||||||
116 | A list of two elements - a reference to an array that contains test | ||||||
117 | objects and a reference to a hash that contains test parameters. | ||||||
118 | |||||||
119 | =cut | ||||||
120 | |||||||
121 | sub parse { | ||||||
122 | 0 | 0 | my $class = shift; | ||||
123 | 0 | my $data = shift; | |||||
124 | |||||||
125 | 0 | my $filter = new WebTestFilter(); # see below | |||||
126 | 0 | my $p = XML::SAX::ParserFactory->parser(Handler => $filter); | |||||
127 | 0 | $p->parse_string($data); | |||||
128 | #FIXME: add $p->parse_string(" |
||||||
129 | 0 | my $cfg = $filter->finalize(); | |||||
130 | |||||||
131 | 0 | return($cfg->{tests}, $cfg->{params}); | |||||
132 | } | ||||||
133 | |||||||
134 | =head2 as_xml ($tests, $params, $opts) | ||||||
135 | |||||||
136 | Given a set of test parameters and global parameters, returns the XML | ||||||
137 | representation of the test script as a string. | ||||||
138 | |||||||
139 | The test definitions and parameters can be obtained from plain C |
||||||
140 | as parsed by L |
||||||
141 | |||||||
142 | =head3 Option nocode | ||||||
143 | |||||||
144 | Forces the replacement of C sections by dummy subroutines. |
||||||
145 | Example: | ||||||
146 | |||||||
147 | $xml = HTTP::WebTest::XMLParser->as_xml( | ||||||
148 | $tests, | ||||||
149 | $param, | ||||||
150 | { nocode => 1 } | ||||||
151 | ); | ||||||
152 | |||||||
153 | =head3 Returns | ||||||
154 | |||||||
155 | The test defintion in XML format. | ||||||
156 | |||||||
157 | =head1 BUGS | ||||||
158 | |||||||
159 | =head3 Method as_xml() | ||||||
160 | |||||||
161 | Any C references in the test object will be replaced by a |
||||||
162 | dummy subroutine if L |
||||||
163 | In order to make this more predictable, you can force this | ||||||
164 | behaviour by specifying option C |
||||||
165 | |||||||
166 | Lists of named parameters are internally stored as array with | ||||||
167 | an even number of elements, rather than a hash. | ||||||
168 | This has the purpose of preserving order of the parameters and | ||||||
169 | also allow more than one parameter with the same name. | ||||||
170 | When such a list is serialized back into XML, the list element | ||||||
171 | contains a list of anonymous parameters, one for each key and | ||||||
172 | value. | ||||||
173 | |||||||
174 | Original test definition: | ||||||
175 | |||||||
176 | |
||||||
177 | text/html,application/xml+html | ||||||
178 | deflate,gzip | ||||||
179 | |||||||
180 | |||||||
181 | Output as: | ||||||
182 | |||||||
183 | |
||||||
184 | Accept | ||||||
185 | text/html,application/xml+html | ||||||
186 | Accept-Encoding | ||||||
187 | deflate,gzip | ||||||
188 | |||||||
189 | |||||||
190 | Both versions are functionally equivalent (just like ',' | ||||||
191 | and '=>' notation are equivalent for Perl hashes). | ||||||
192 | |||||||
193 | =cut | ||||||
194 | |||||||
195 | sub as_xml { | ||||||
196 | 0 | 0 | my $class = shift; | ||||
197 | 0 | my ($tests, $params, $opt) = @_; | |||||
198 | |||||||
199 | 0 | my $writer = new WebTestWriter($opt); | |||||
200 | 0 | $writer->as_xml($tests, $params); | |||||
201 | } | ||||||
202 | |||||||
203 | =head1 COPYRIGHT | ||||||
204 | |||||||
205 | Copyright (c) 2002 - 2003 Johannes la Poutre. All rights reserved. | ||||||
206 | |||||||
207 | This program is free software; you can redistribute it and/or modify | ||||||
208 | it under the same terms as Perl itself. | ||||||
209 | |||||||
210 | =head1 SEE ALSO | ||||||
211 | |||||||
212 | L |
||||||
213 | |||||||
214 | L |
||||||
215 | |||||||
216 | L |
||||||
217 | |||||||
218 | Examples are in directory 't' from the distribution, the DTD and | ||||||
219 | utility scripts are in subdir 'scripts' from the distribution. | ||||||
220 | |||||||
221 | =cut | ||||||
222 | |||||||
223 | ################################################## SAX handler class ### | ||||||
224 | package WebTestFilter; | ||||||
225 | 3 | 3 | 17 | use strict; | |||
3 | 4 | ||||||
3 | 107 | ||||||
226 | 3 | 3 | 13 | use base qw(XML::SAX::Base); | |||
3 | 5 | ||||||
3 | 5659 | ||||||
227 | 3 | 3 | 60188 | use Carp qw(croak); | |||
3 | 9 | ||||||
3 | 222 | ||||||
228 | 3 | 3 | 11474 | use HTTP::WebTest::Utils qw(eval_in_playground make_sub_in_playground); | |||
0 | |||||||
0 | |||||||
229 | |||||||
230 | sub new { | ||||||
231 | my $class = shift; | ||||||
232 | # my %opt = @_; # parser options | ||||||
233 | my $self = {}; | ||||||
234 | $self->{tests} = [()]; # test definitions | ||||||
235 | $self->{params} = {}; # global params | ||||||
236 | $self->{stack} = {}; # stack for current test node | ||||||
237 | $self->{name} = ''; # current element name | ||||||
238 | $self->{context} = [()]; # XML element stack | ||||||
239 | return bless $self, $class; | ||||||
240 | } | ||||||
241 | |||||||
242 | sub characters { | ||||||
243 | my $self = shift; | ||||||
244 | my ($chars) = @_; | ||||||
245 | $self->{charbuf} .= $chars->{Data}; | ||||||
246 | } | ||||||
247 | |||||||
248 | sub start_element { | ||||||
249 | my $self = shift; | ||||||
250 | my ($elt) = @_; | ||||||
251 | my $element = $elt->{Name}; | ||||||
252 | my $parent = $self->{context}->[-1] || ''; | ||||||
253 | if (($parent eq 'param') || ($parent eq 'code')) { | ||||||
254 | $self->_croak(sprintf 'No child elements allowed for element "<%s/>"', $parent); | ||||||
255 | } | ||||||
256 | $self->{charbuf} = ''; # reset character buffer | ||||||
257 | # we have 4 relevant events: | ||||||
258 | # - param with name attribute | ||||||
259 | # - list context: pair of 2 scalars (preserve list order) | ||||||
260 | # - scalar context: hash (key, value) pair | ||||||
261 | # - param (unnamed) | ||||||
262 | # - list context: single value | ||||||
263 | # - named list | ||||||
264 | # - scalar context: named array (hash key, value = arrayref) | ||||||
265 | # - list (unnamed) | ||||||
266 | # - list context: (anonymous) arrayref | ||||||
267 | # character data is handled in end_element | ||||||
268 | my $name = $elt->{Attributes}->{'{}name'}->{Value}; | ||||||
269 | #printf "Elt: %s, Name: %s, Context: %s\n", $element, $name || '-', join('/', @{$self->{context}}); | ||||||
270 | if (($element eq 'param') || ($element eq 'code')) { | ||||||
271 | if (defined $name) { | ||||||
272 | if ($parent eq 'list') { # named param, list context | ||||||
273 | # push param name as list element | ||||||
274 | # character data handled in end_element | ||||||
275 | if (ref $self->{stack}->{$self->{name}}->[-1] eq 'ARRAY') { | ||||||
276 | # Nested list (LoL): | ||||||
277 | push @{ $self->{stack}->{$self->{name}}->[-1] }, $name; | ||||||
278 | $self->{sp} = $self->{stack}->{$self->{name}}->[-1]; | ||||||
279 | } else { | ||||||
280 | # plain (top level) list: | ||||||
281 | push @{ $self->{stack}->{$self->{name}} }, $name; | ||||||
282 | $self->{sp} = $self->{stack}->{$self->{name}}; | ||||||
283 | } | ||||||
284 | } else { # named param, scalar context | ||||||
285 | # keep track of last name (= hash key) | ||||||
286 | $self->{name} = $name; | ||||||
287 | $self->{sp} = $self->{stack}->{$self->{name}}; | ||||||
288 | # character data will be assigned to | ||||||
289 | # $self->{stack}->{$self->{name}} in end_element | ||||||
290 | } | ||||||
291 | } else { # unnamed param (list context) | ||||||
292 | # character data only; handled in end_element | ||||||
293 | if (! $parent eq 'list') { | ||||||
294 | $self->_croak('Invalid unnamed param in scalar context'); | ||||||
295 | } | ||||||
296 | $self->{sp} = $self->{stack}->{$self->{name}}; | ||||||
297 | } | ||||||
298 | } elsif ($element eq 'list') { | ||||||
299 | if (defined $name) { # named list | ||||||
300 | if ($parent eq 'list') { | ||||||
301 | $self->_croak('Invalid named list in list context'); | ||||||
302 | } | ||||||
303 | # create empty named list, hash key = name | ||||||
304 | $self->{sp} = $self->{stack}->{$name} = [()]; | ||||||
305 | # keep track of last name (= hash key) | ||||||
306 | $self->{name} = $name; | ||||||
307 | } else { # unnamed list | ||||||
308 | # anonymous list, push ref. to higher level list | ||||||
309 | push @{ $self->{stack}->{$self->{name}} }, [()]; | ||||||
310 | $self->{sp} = $self->{stack}->{$self->{name}}; | ||||||
311 | } | ||||||
312 | } elsif ($parent eq 'WebTest') { | ||||||
313 | # create a new stack for each second level element (test or params) | ||||||
314 | $self->{sp} = $self->{stack} = {}; | ||||||
315 | } elsif ($element eq 'WebTest') { | ||||||
316 | # root element, validate version attribute | ||||||
317 | my $version = $elt->{Attributes}->{'{}version'}->{Value} || '0'; | ||||||
318 | if ($version < $webtest_definition_version) { | ||||||
319 | $self->_croak("WebTest definition should be version $webtest_definition_version or newer"); | ||||||
320 | } | ||||||
321 | } else { | ||||||
322 | # $self->_croak(sprintf('Unexpected element <%s>', $element)); | ||||||
323 | } | ||||||
324 | push @{$self->{context}}, $element; | ||||||
325 | return; | ||||||
326 | } | ||||||
327 | |||||||
328 | sub end_element { | ||||||
329 | my $self = shift; | ||||||
330 | my ($elt) = @_; | ||||||
331 | my $element = $elt->{Name}; | ||||||
332 | if ($element eq 'code') { | ||||||
333 | $self->{charbuf} = make_sub_in_playground($self->{charbuf}); | ||||||
334 | } | ||||||
335 | if ($element eq 'test') { | ||||||
336 | push @{ $self->{tests} }, $self->{stack}; | ||||||
337 | } elsif ($element eq 'params') { | ||||||
338 | $self->{params} = $self->{stack}; | ||||||
339 | } elsif (($element eq 'param') || ($element eq 'code')) { | ||||||
340 | if (ref $self->{sp} eq 'ARRAY') { | ||||||
341 | # list parameter: push character buffer on stack | ||||||
342 | push @{ $self->{sp} }, $self->{charbuf}; | ||||||
343 | } else { | ||||||
344 | # plain scalar parameter: assign character buffer | ||||||
345 | $self->{stack}->{$self->{name}} = $self->{charbuf}; | ||||||
346 | } | ||||||
347 | } elsif ($element eq 'list') { | ||||||
348 | $self->_croak('Invalid character data in "list" element') if ($self->{charbuf} =~ /[^\s]/); | ||||||
349 | } | ||||||
350 | pop @{$self->{context}}; | ||||||
351 | $self->{charbuf} = ''; | ||||||
352 | } | ||||||
353 | |||||||
354 | # initialize Locator (for error messages) | ||||||
355 | sub set_document_locator { | ||||||
356 | my $self = shift; | ||||||
357 | $self->{locator} = shift; | ||||||
358 | } | ||||||
359 | |||||||
360 | sub _croak { | ||||||
361 | my $self = shift; | ||||||
362 | my $msg = shift; | ||||||
363 | croak sprintf("%s [Ln: %s, Col: %s]\n", | ||||||
364 | $msg, | ||||||
365 | $self->{locator}->{LineNumber} || 'N.A.', # Expat: no set_document_locator() | ||||||
366 | $self->{locator}->{ColumnNumber} || 'N.A.', | ||||||
367 | ); | ||||||
368 | } | ||||||
369 | |||||||
370 | sub finalize { | ||||||
371 | my $self = shift; | ||||||
372 | return { params => $self->{params}, tests => $self->{tests} }; | ||||||
373 | } | ||||||
374 | |||||||
375 | ################################################## Webtest Writer ### | ||||||
376 | package WebTestWriter; | ||||||
377 | use strict; | ||||||
378 | use XML::Writer; | ||||||
379 | use IO::Scalar; | ||||||
380 | use Carp qw(croak carp); | ||||||
381 | |||||||
382 | sub new { | ||||||
383 | my $class = shift; | ||||||
384 | my $opt = shift; | ||||||
385 | my $self = {}; | ||||||
386 | $self->{deparse} = 0 if $opt->{nocode}; | ||||||
387 | $self->{buffer} = ''; | ||||||
388 | my $out = new IO::Scalar(\$self->{buffer}); | ||||||
389 | $self->{xh} = new XML::Writer(OUTPUT => $out, | ||||||
390 | DATA_MODE => 1, | ||||||
391 | DATA_INDENT => 2 | ||||||
392 | ); | ||||||
393 | return bless $self; | ||||||
394 | } | ||||||
395 | |||||||
396 | # as_xml: writes out test definitions and parameters as XML | ||||||
397 | # plain hash {key, val} is output as val | ||||||
398 | # list ref: |
||||||
399 | # anonymous params/lists lack name attribute | ||||||
400 | sub as_xml { | ||||||
401 | my $self = shift; | ||||||
402 | my ($tests, $params) = @_; | ||||||
403 | $self->{xh}->xmlDecl(); | ||||||
404 | $self->{xh}->startTag('WebTest', version => $webtest_definition_version); | ||||||
405 | $self->_serialize('params', $params); | ||||||
406 | foreach my $test (@$tests) { | ||||||
407 | $self->_serialize('test', $test); | ||||||
408 | } | ||||||
409 | $self->{xh}->endTag('WebTest'); | ||||||
410 | $self->{xh}->end(); | ||||||
411 | return $self->{buffer}; | ||||||
412 | } | ||||||
413 | |||||||
414 | # take a hash ref and serialize to xml in element $elt | ||||||
415 | sub _serialize { | ||||||
416 | my $self = shift; | ||||||
417 | my ($elt, $ref) = @_; | ||||||
418 | $self->{xh}->startTag($elt); | ||||||
419 | # sort hash to get more predictable output | ||||||
420 | foreach my $key (sort keys %$ref) { | ||||||
421 | my $val = $ref->{$key}; | ||||||
422 | if ((ref $val) && (ref $val eq 'ARRAY')) { # list ref | ||||||
423 | $self->_list($key, $val); | ||||||
424 | } elsif ((ref $val) && (ref $val eq 'HASH')) { # only from parsed wtscipt | ||||||
425 | $self->_hlist($key, $val); | ||||||
426 | } else { | ||||||
427 | $self->_param($key, $val); | ||||||
428 | } | ||||||
429 | } | ||||||
430 | $self->{xh}->endTag($elt); | ||||||
431 | } | ||||||
432 | |||||||
433 | # lists can be nested | ||||||
434 | sub _list { | ||||||
435 | my $self = shift; | ||||||
436 | my ($key, $val) = @_; | ||||||
437 | if (defined $key) { | ||||||
438 | $self->{xh}->startTag('list', name => $key); # named list | ||||||
439 | } else { | ||||||
440 | $self->{xh}->startTag('list'); # anon list | ||||||
441 | } | ||||||
442 | foreach my $elt (@$val) { | ||||||
443 | if ((ref $elt) && (ref $elt eq 'ARRAY')) { | ||||||
444 | $self->_list(undef, $elt); # nested anon list; recurse | ||||||
445 | } else { | ||||||
446 | # At this stage we don't know the difference | ||||||
447 | # between a flattened hash or a list of scalar elements. | ||||||
448 | # The latter is more safe (odd element count)... | ||||||
449 | $self->_param(undef, $elt); # anon param | ||||||
450 | } | ||||||
451 | } | ||||||
452 | $self->{xh}->endTag('list'); | ||||||
453 | } | ||||||
454 | |||||||
455 | # hash list; can contain list | ||||||
456 | sub _hlist { | ||||||
457 | my $self = shift; | ||||||
458 | my ($key, $val) = @_; | ||||||
459 | if (defined $key) { | ||||||
460 | $self->{xh}->startTag('list', name => $key); # named list | ||||||
461 | } else { | ||||||
462 | $self->{xh}->startTag('list'); # anon list | ||||||
463 | } | ||||||
464 | # sort hash to get more predictable output | ||||||
465 | foreach my $lkey (sort keys %$val) { | ||||||
466 | my $lval = $val->{$lkey}; | ||||||
467 | if ((ref $lval) && (ref $lval eq 'ARRAY')) { | ||||||
468 | $self->_list($lkey, $lval); | ||||||
469 | } else { | ||||||
470 | $self->_param($lkey, $lval); | ||||||
471 | } | ||||||
472 | } | ||||||
473 | $self->{xh}->endTag('list'); | ||||||
474 | } | ||||||
475 | |||||||
476 | # params contain scalar data or code ref, no recursion | ||||||
477 | sub _param { | ||||||
478 | my $self = shift; | ||||||
479 | my ($key, $val) = @_; | ||||||
480 | my $tag = 'param'; | ||||||
481 | if ($val && (ref $val eq 'CODE')) { | ||||||
482 | $tag = 'code'; | ||||||
483 | if (! defined $self->{deparse}) { | ||||||
484 | eval { | ||||||
485 | local $SIG{__DIE__}; | ||||||
486 | require B::Deparse; # as of Perl 5.6 | ||||||
487 | my $vers = $B::Deparse::VERSION || 0; | ||||||
488 | die "B::Deparse 0.60 or newer needed, installed version is $vers" if ($vers < 0.60); | ||||||
489 | }; | ||||||
490 | if ($@) { | ||||||
491 | carp($@ . "Couldn't load B::Deparse, CODE blocks will be skipped"); | ||||||
492 | $self->{deparse} = 0; | ||||||
493 | } else { | ||||||
494 | $self->{deparse} = new B::Deparse; # initialize deparser | ||||||
495 | } | ||||||
496 | } | ||||||
497 | $val = ($self->{deparse}) ? $self->{deparse}->coderef2text($val) | ||||||
498 | : "sub { 'CODE N.A.' }"; | ||||||
499 | } | ||||||
500 | if (defined $key) { | ||||||
501 | $self->{xh}->startTag($tag, name => $key); # named param | ||||||
502 | } else { | ||||||
503 | $self->{xh}->startTag($tag); # anon param | ||||||
504 | } | ||||||
505 | $self->{xh}->characters($val || ''); | ||||||
506 | $self->{xh}->endTag($tag); | ||||||
507 | } | ||||||
508 | |||||||
509 | |||||||
510 | 1; | ||||||
511 | __END__ |