File Coverage

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 element
40             - test definitions are in elements
41             - a list is represented by a element
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("") and $p->parse_uri("test.xml");
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 is missing from your installation.
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__