#!/usr/local/bin/perl # File: HTTPtestS.pl # # Author: G. Wade Johnson # # Copyright 2001 by Telescan, Inc. # Released under the same license as Perl. # # Warning: This program is under construction. # It may be changed at any time. use FindBin; use LWP::UserAgent; use XML::Parser; use URI::Escape; use Getopt::Std; use File::Spec; $VERSION = 0.68; my $CompileMsg = ''; BEGIN { $^W = 1; if($^O eq 'MSWin32') { # Windows does not expand wildcards on the command line # So we expand wildcards if we find them. # However, in 5.6, glob fails to expand wildcards that include a # UNC, such as \\server\share\dir\*.* my @files; @ARGV = map { /[\*\?]/ && (@files = glob $_) ? @files : $_ } @ARGV if $] > 5.00307; } #----------------------------------------------------------------- # Attempt to 'use' the Time::HiRes module. If it is not available, # we continue. The module is not necessary, it just gives better # resolution if we have it. eval { require Time::HiRes; Time::HiRes->import( 'time' ); }; $CompileMsg .= "HiRes Timing not available.\n" if $@; } use strict; use vars qw/%opts %Options $VERSION/; getopts( 'vwqVlo:I:f:', \%opts ) || usage(); if($opts{V}) { print "$0: Version $VERSION\n"; exit( 0 ); } my $ResponseTime = 0; my $DefaultLibDir = "$FindBin::Bin/lib"; my %Params = (); my %CodeLibIDs = (); my %CmdLine = (); my $ScriptStart = time; extract_cmd_line(); expand_list_file(); usage() unless @ARGV; print STDERR $CompileMsg if $CompileMsg and 'verbose' eq $Options{msgs}; my $parser = new XML::Parser( Style=>'Objects', Pkg=>'tst' ); XMLdata::set_package( 'tst' ); my $ua = new LWP::UserAgent; die "Unable to create user agent.\n" unless $ua; #----------------------------------------------- # Define handling of top-level elements my %TSElement = ( param => sub { }, # already handled test => sub { run_test( $ua, @_ ); }, session => \&execute_a_session, codelib => \&compile_lib, include => sub { }, # already handled option => sub { die "An option element found out of place.\n"; } ); XMLdata::DeclareElements( qw/testset param test session codelib include inclusion option/ ); #----------------------------------------------- # Define handling of test criteria elements my %Tests = ( # general 'not' => \&validate_not, 'or' => \&validate_or, 'and' => \&validate_and, # protocol-specific status => \&validate_status, header => \&validate_header, content => \&validate_content, 'time' => \&validate_time, code => \&validate_code, msg => \&send_msg, ); XMLdata::DeclareElements( qw/request response body not or and status header content time code msg/ ); my ($tots, $tot, $sets) = (0, 0, 0, 0); #----------------------------------------------- # Main file processing loop. # invariant: @ARGV contains testset filenames to process. while(@ARGV) { my $testfile = shift; my $doc = $parser->parsefile( $testfile ) or die "Unable to read tests file \'$testfile\'.\n"; my $testset = XMLdata::root( $doc ) or die "Unable to read tests file \'$testfile\'.\n"; # Extract options extract_options( $testset ); add_command_line_overrides(); open_logfile(); # Expand includes at this point. expand_includes( $testset ); my $err = invalid_testset( $testset ); die "Testset \"$testfile\" has errors: $err\n" if $err; %Params = (); my $succ = 0; my $tests = count_tests( $testset ); printlog( '# ', $testset->{label}, "\n" ); printlogv( "1..$tests\n" ); setup_params( $testset ); eval { $succ += process_testset( $testset->get_children() ); }; if($@) { die $@ if $@ ne "msg-stop\n"; print "\n"; } else { if($succ != $tests) { printlog( "Failed ", ($tests-$succ), "/$tests\n" ); } } $tots += $succ; $tot += $tests; ++$sets; close_logfile(); } my $ScriptTime = time - $ScriptStart; if($tots == $tot) { if(1 < $sets) { printlog( "All tests successful.\nFiles=$sets, Tests=$tot.\n" ); } else { printlog( "All tests successful.\nTests=$tot.\n" ); } } else { printlog( "FAILED tests\n\tFailed ", ($tot-$tots), "/$tot\n" ); } printlog( sprintf "# Elapsed time: %.2f seconds\n", $ScriptTime ); #-------------------------------------------------------- # Count the test elements in the supplied testset # # Input: $testset - the testset object to count # # Output: number of test elements found in testset sub count_tests { my $testset = shift; return scalar( @{$testset->get_descendents_by_tag( 'test' )} ); } #-------------------------------------------------------- # Process several tests # # Input: A list of top-level elements to process. # # Output: the number of successful tests sub process_testset { my ($Succ, $numTest) = process_set_of_tests( 0, @_ ); $Succ; } #-------------------------------------------------------- # Process several tests # # Input: $numTest - current test number # A list of top-level elements to process. # # Output: a list containing the number of successful tests # and the current test number. sub process_set_of_tests { my $Succ = 0; my $numTest = shift; # invariant: @_ contains the remaining top-level test elements foreach my $el (@_) { next unless $el->is_element(); my $tag = $el->tagname(); if(exists $TSElement{$tag}) { $TSElement{$tag}->( $el, $numTest, $Succ ); } else { die "Unrecognized element '$tag'.\n"; } } ($Succ, $numTest) } #-------------------------------------------------------- # Initialize Options variables sub initialize_options { %Options = ( incpath => [], logging => 0, logfile => 'HTTPtest.log', msgs => '', ); } #-------------------------------------------------------- # Extract the option elements, setting the appropriate # internal variables # # Input: reference to the testset object # # Currently supported options # incpath - include path (;-separated list) # logging - turn logging on/off (yes|no) # logfile - define file to log output # msgs - specify how verbose the output (verbose|quiet)? sub extract_options { my $testset = shift; my $children = $testset->get_children(); initialize_options(); # reset options variables for each new file while(@{$children}) { my $child = shift @{$children}; next unless $child->is_element(); if('option' ne $child->tagname()) { unshift @{$children}, $child; return; } # need to drive this with a data structure. if('incpath' eq $child->{name}) { push @{$Options{$child->{name}}}, split( ';', $child->{value} ); } elsif('logging' eq $child->{name}) { $Options{$child->{name}} = ('yes' eq $child->{value}); } else { $Options{$child->{name}} = $child->{value}; } } push @{$Options{incpath}}, $DefaultLibDir; } #-------------------------------------------------------- # Override the internal options based on the command line # options. sub add_command_line_overrides { $Options{msgs} = '' if defined $opts{w}; $Options{msgs} = 'verbose' if defined $opts{v}; $Options{msgs} = 'quiet' if defined $opts{q}; $Options{logging} = defined $opts{l} || defined $opts{o}; $Options{logfile} = $opts{o} if defined $opts{o}; # Reverse needed to keep search order intact. unshift @{$Options{incpath}}, split( ';', $opts{I} ) if defined $opts{I}; } #-------------------------------------------------------- # Perform one test from the testset and report on # results # # Input: $ua - reference to an LWP::UserAgent object # $test - reference to a test object to execute # numTests - current test number # succ - number of successful tests # # Output: boolean, true if test succeeded, false otherwise sub run_test { my $ua = shift; my $test = shift; my $label = $test->{label} ? " - $test->{label}" : ''; $_[0]++; # modify numTests directly if(evaluate_test( $ua, $test )) { printlogv( "ok $_[0]$label\n" ); $_[1]++; } else { printlogv( "not ok $_[0]$label\n" ); } } #-------------------------------------------------------- # Perform one test from the testset # # Input: $ua - reference to an LWP::UserAgent object # $test - reference to a test object to execute # # Output: boolean, true if test succeeded, false otherwise sub evaluate_test { my $ua = shift; my $test = shift; my $treq = $test->get_child_by_tag( 'request', 0 ); my $url = resolve_params( $treq->{url} ); my $method = $treq->{method} || "GET"; my $req; printlogvv( "\n $method $url\n" ); my $hdr = new HTTP::Headers; my @headers = $treq->get_children_by_tag( 'header' ); foreach my $h (@headers) { $hdr->header( $h->{name} => $h->{value} ) if $h->{name}; } if($method eq 'POST') { my ($body, $type) = extract_body_from_request( $treq ); $hdr->header( 'Content-Type' => $type ) if $type; $req = new HTTP::Request( $method, $url, $hdr, $body ); } else { $req = new HTTP::Request( $method, $url, $hdr ); } my $StartTime = time; my $resp = $ua->request( $req ); $ResponseTime = time - $StartTime; my $bValid = validate_response( $test, $resp ); $bValid; } #-------------------------------------------------- # Extract the text from the elements of the # supplied request and return a string which is used # as the content of the request. # # Input: $treq - the request element # # Output: string representing the content of the request sub extract_body_from_request { my $treq = shift; # ??? need to modify to deal with multiple body elements my @belem = $treq->get_children_by_tag( 'body' ); return ('', '') unless @belem; my $body = ''; my $type = ''; # ??? change into loop and deal with boundaries $body = resolve_params( $belem[0]->get_text_content() ); $type = $belem[0]->{type} if $belem[0]->{type}; # ??? need to test for the standard encoding if(exists $belem[0]->{encode} and 'yes' eq $belem[0]->{encode}) { $body = uri_escape( $body ); $body = s/ /\+/g; } ($body, $type); } #-------------------------------------------------- # Validate the HTTP response # # Input: $test - reference to the test to execute # $resp - reference to an HTTP::Response object resulting # from an HTTP request # # Output: boolean, true if test is successful, false otherwise sub validate_response { my $test = shift; my $resp = shift; my $tresp = $test->get_child_by_tag( 'response', 0 ); my $result = validate_and( $tresp, $resp, 'response' ); unless($result->{success}) { printlogvv( "failed, $result->{'short'}.\n" ); printlogvv( $result->{'long'}, "\n" ) if $result->{'long'}; return 0; } $result->{'success'}; } #-------------------------------------------------- sub is_regex_match { my $elem = shift; defined $elem and (exists $elem->{match} and 'regexp' eq $elem->{match}); } #-------------------------------------------------- sub is_exist_match { my $elem = shift; defined $elem and (!exists $elem->{match} or 'exist' eq $elem->{match}); } #-------------------------------------------------- sub is_nonexist_match { my $elem = shift; defined $elem and (exists $elem->{match} and 'nonexist' eq $elem->{match}); } #-------------------------------------------------- # Validate a single content test # # Input: $ct - reference to a content test criterion object # $resp - reference to an HTTP::Response object resulting # from an HTTP request # # Output: reference to a hash containing the result information sub validate_content { my $ct = shift; my $resp = shift; my $content = ''; my %result = ( success => 1, short => 'content matches', long => '' ); $content = resolve_params( $ct->get_text_content() ) || ''; unless(string_matches( $ct, $resp->content, $content )) { $result{'short'} = "content does not match"; $result{'long'} = "Expected:\n$content\nReturned:\n" . $resp->content; $result{'success'} = 0; } \%result; } #-------------------------------------------------- # Perform a string test for a test criterion object # # Input: $ct - reference to a test criterion object # $returned - value of a response header # $expected - string content expected # # Output: true if match, false otherwise sub string_matches { my $ct = shift; my $returned = shift; my $expected = shift; (is_regex_match($ct) ? ($returned =~ m/$expected/sm) : ($returned eq $expected)); } #-------------------------------------------------- # Validate a single HTTP header test # # Input: $hdr - reference to a header test criterion object # $resp - reference to an HTTP::Response object resulting # from an HTTP request # # Output: reference to a hash containing the result information sub validate_header { my $hdr = shift; my $resp = shift; my %result = ( success => 1, short => 'header matches', long => '' ); my $name = $hdr->{name}; my $value = $hdr->{value}; my $rethdr = $resp->header( $name ); if(!defined $rethdr) { $result{short} = "header \'$name\' is not present"; $result{success} = 0 unless is_nonexist_match( $hdr ); return \%result; } return \%result if is_exist_match( $hdr ); unless(string_matches( $hdr, $rethdr, $value )) { $result{'success'} = 0; $result{'short'} = "header mismatch"; $result{'long'} = "expected \'$value\', received \'$rethdr\'"; } \%result; } #-------------------------------------------------- # Validate a HTTP status code test # # Input: $st - reference to a status code test criterion object # $resp - reference to an HTTP::Response object resulting # from an HTTP request # # Output: reference to a hash containing the result information sub validate_status { my $st = shift; my $resp = shift; my %result = ( success => 1, short => 'status code matches', long => '' ); my $code = $st->{code}; unless(string_matches( $st, $resp->code, $code)) { $result{'success'} = 0; $result{'short'} = 'status code mismatch'; $result{'long'} = "expected \'$code\', received \'" . $resp->code . "\'"; } \%result; } #-------------------------------------------------- # handle group of tests that must all succeed for success # # Input: $cond - reference to an and test criterion object # $resp - reference to an HTTP::Response object resulting # from an HTTP request # # Output: reference to a hash containing the result information sub validate_and { my $cond = shift; my $resp = shift; my $result = { success => 1, short => 'tests successful', long => '' }; foreach my $tst ($cond->get_children()) { next unless $tst->is_element(); my $res = perform_test( $tst, $resp ); $result->{'long'} .= "\n -- and --\n" if $result->{'long'}; $result->{'long'} .= $res->{'short'}; $result->{'long'} .= "\n".$res->{'long'} if $res->{'long'}; return $res unless $res->{'success'}; } $result; } #-------------------------------------------------- # handle group of tests that one must succeed for success # # Input: $cond - reference to an or test criterion object # $resp - reference to an HTTP::Response object resulting # from an HTTP request # # Output: reference to a hash containing the result information sub validate_or { my $cond = shift; my $resp = shift; my $result = { success => 0, short => 'tests failed', long => '' }; foreach my $tst ($cond->get_children()) { next unless $tst->is_element(); my $res = perform_test( $tst, $resp ); $result->{'long'} .= "\n -- or --\n" if $result->{'long'}; $result->{'long'} .= $res->{'short'}; $result->{'long'} .= "\n".$res->{'long'} if $res->{'long'}; return $res if $res->{'success'}; } $result; } #-------------------------------------------------- # handle a test that must fail for success # # Input: $not - reference to a not test criterion object # $resp - reference to an HTTP::Response object resulting # from an HTTP request # # Output: reference to a hash containing the result information sub validate_not { my $not = shift; my $resp = shift; my $result = undef; my $tst = (grep { $_->is_element() } $not->get_children())[0]; $result = perform_test( $tst, $resp ); $result->{'long'} = "not ($result->{'long'})" if $result->{'long'}; $result->{'short'} = "not ($result->{'short'})"; $result->{'success'} = !$result->{'success'}; $result; } #-------------------------------------------------- # test response time of the request # # Input: $tst - reference to a time test criterion object # $resp - reference to an HTTP::Response object resulting # from an HTTP request # # Output: reference to a hash containing the result information sub validate_time { my $tst = shift; my $resp = shift; my %result = ( success => 1, short => 'response time in range', long => '' ); if(exists $tst->{min} and $tst->{min} and $ResponseTime < $tst->{min}) { $result{'short'} = "response time shorter than expected"; $result{'long'} = "Expected: $tst->{min} secs, returned: $ResponseTime secs"; $result{'success'} = 0; } if(exists $tst->{max} and $tst->{max} and $ResponseTime > $tst->{max}) { $result{'short'} = "response time longer than expected"; $result{'long'} = "Expected: $tst->{max} secs, returned: $ResponseTime secs"; $result{'success'} = 0; } \%result; } #-------------------------------------------------- # execute code to test response # # Input: $tst - reference to a code test criterion object # $resp - reference to an HTTP::Response object resulting # from an HTTP request # # Output: reference to a hash containing the result information sub validate_code { my $tst = shift; my $resp = shift; my $label = $tst->{label} || ''; my %result = ( success => 1, short => "code test '$label' successful", long => '' ); my @ret = (1, '', ''); my $code = resolve_params( $tst->get_text_content() ) || ''; { local $SIG{__WARN__} = sub { my $m = shift; $m =~ s/at \(eval \d+\)/in code test '$label' at/g; warn $m; }; $code =~ s/<//gsm; $code =~ s/'/'/gsm; @ret = eval $code; } if($@) { $@ =~ s/\(eval \d+\) //g; die "Code($label) failed: $@\n"; } $result{success} = $ret[0]; $result{short} = $ret[1] || ($ret[0] ? "code test '$label' successful" : "code test '$label' failed" ); $result{long} = $ret[2] || ''; \%result; } #-------------------------------------------------- # pseudo-test criterion for sending messages or aborting # on a really critical error. # # Input: $tst - reference to a msg pseudo-test criterion object # $resp - reference to an HTTP::Response object resulting # from an HTTP request # # Output: reference to a hash containing the result information sub send_msg { my $tst = shift; my $resp = shift; my %result = ( success => exists $tst->{fail} ? 'no' eq $tst->{fail} : 0, short => 'msg encountered', long => '' ); $result{success} = msg( $tst->get_text_content(), $tst->{print}, $tst->{action} ); \%result; } #-------------------------------------------------- # Supply the actual function of the msg pseudo-test criterion. # Print the supplied message under the appropriate condition, and # take the supplied action. # # Input: $msg - message to display # $print - specify conditions under which to print # $action - specify action to take after printing # # Output: a boolean telling if the action was success or fail sub msg { my $msg = shift || ''; my $print = shift || 'normal'; my $action = shift || 'fail'; $msg = resolve_params( $msg ); $msg =~ s/[ \t]+$//s; PRINT: { 'always' eq $print && do { printlog( $msg ); last PRINT; }; 'normal' eq $print && do { printlogv( $msg ); last PRINT; }; 'verbose' eq $print && do { printlogvv( $msg ); last PRINT; }; }; ACTION: { 'success' eq $action && do { return 1; }; 'fail' eq $action && do { return 0; }; 'stop' eq $action && do { die "msg-stop\n"; }; 'exit' eq $action && do { exit( 1 ); }; }; } #-------------------------------------------------- # Vector to the correct kind of test # # Input: $test - reference to a test criterion object # $resp - reference to an HTTP::Response object resulting # from an HTTP request # # Output: reference to a hash containing the result information sub perform_test { my $test = shift; my $resp = shift; my $tag = $test->tagname(); my $label = $test->{label} || $tag; printlogvv( "Test: $label\n" ); $Tests{$tag}->( $test, $resp ); } #-------------------------------------------------- # Read parameters from the testset and # command line # # Input: $ts - reference to a testset object sub setup_params { my $ts = shift; my $param; foreach $param ($ts->get_children_by_tag( 'param' )) { my $name = $param->{name}; die "Invalid param name \'$name\'.\n" unless $name =~ /^\w+$/; my $value = $param->{value}; $value = $CmdLine{$name} if exists $CmdLine{$name}; $Params{$name} = resolve_params( $value ); } foreach $param (keys %CmdLine) { $Params{$param} = $CmdLine{$param}; } } #-------------------------------------------------- # Replace all parameters found in the supplied # string with the value of those parameters. # # Input: $str - string possibly containing replaceable parameters # # Output: string with replaceable parameters resolved sub resolve_params { my $str = shift; if($str) { # If param replace with param value, otherwise exec Perl expr. $str =~ s/{{([^}]+)}}/expand_a_param( $1 )/smeg; } $str; } #------------------------------------------------- # Take a potential parameter reference and expand # it either as the value of the parameter or as a # Perl expression. # # Input: $parmref - a suspected parameter reference # # Output: the expanded text of the parameter or # result of the Perl expression sub expand_a_param { my $parmref = shift; unless($parmref =~ /^\w+$/) # doesn't appear to be a parameter { return eval $parmref; } elsif(exists $Params{$parmref}) # the parameter exists { return $Params{$parmref}; } else # looks like a parameter, it's not { die "Reference of unknown parameter \'$parmref\'\n"; } } #-------------------------------------------------- # Extract parameters of the form 'name=value' from # the command line. sub extract_cmd_line { my @args=(); foreach(@ARGV) { if(/^([^=]+)=([^=]*)$/) { $CmdLine{$1} = $2; } else { push @args, $_; } } @ARGV = @args; } #-------------------------------------------------- # If the -f command line option has been used to # supply a file listing testsets, expand that file # onto the command line. sub expand_list_file { return unless defined $opts{f} and -e $opts{f}; local *L; local $_; open( L, $opts{f} ) or die "Unable to open list file '$opts{f}'.\n"; my @testsets = (); while() { s/#.*//; next if /^\s*$/; push @testsets, split( /\s+/, $_ ); } close( L ); unshift @ARGV, @testsets; } #--------------------------------------------------- # Compile library code which may be used elsewhere # in a test. # # Input: $code - reference to a codelib to compile # numTests - current test number # Succ - number of successful tests sub execute_a_session { my $session = shift; my $label = $session->{label} || 'unlabeled session'; printlogv( "# Starting '$label'\n" ); # Startup a session my ($Succ, $numTest) = process_set_of_tests( $_[0], $session->get_children() ); $_[0] = $numTest; $_[1] += $Succ; # Cleanup the session printlogv( "# Ending '$label'\n" ); } #--------------------------------------------------- # Compile library code which may be used elsewhere # in a test. # # Input: $code - reference to a codelib to compile sub compile_lib { my $code = shift; my $label = $code->{label}; printlogvv( "Compile codelib ", ($code->{label} || 'unkown'), (exists $code->{id} ? "($code->{id})\n" : "\n") ); if(exists $code->{id}) { my $id = $code->{id}; if(exists $CodeLibIDs{$id}) { printlogvv( "codelib ($id) already compiled\n" ); return 1; } $CodeLibIDs{$id} = 1; } { # signal handler to clean up compile warnings local $SIG{__WARN__} = sub { my $m = shift; $m =~ s/at \(eval \d+\)/in codelib '$label' at/g; warn $m; }; my $content = resolve_params( $code->get_text_content() ); $content =~ s/<//gsm; $content =~ s/'/'/gsm; eval $content; } if($@) { $@ =~ s/\(eval \d+\) //g; die "Code($label) failed: $@\n" ; } } #-------------------------------------------------------- # Search the current directory and include path to find # an include file # # Input: $file - name of file to find # # Output: filename qualified with the appropriate path. # If the filename is not found, the original name # is returned sub find_include_file { my $file = shift; return $file if File::Spec->file_name_is_absolute( $file ) or -e $file; foreach my $dir (@{$Options{incpath}}) { my $f = File::Spec->catfile( $dir, $file ); return $f if -e $f; } return $file; } #--------------------------------------------------------------- # Return the content of the root element in the file referenced # by the supplied include element. Recursively expand includes # in the children. # # Input: $inc - reference to an include object # # Output: one or more xml objects to replace the include object sub expand_an_include { my $inc = shift; my $file = find_include_file( $inc->{file} ); printlogvv( "Including ", ($inc->{label} || $file || 'unkown'), "\n" ); die "Missing or empty file attribute.\n" unless $file; if(exists $inc->{parse} and 'text' eq $inc->{parse}) { local *I; local $/ = undef; open( I, $file ) or die "Unable to open include file '$file'.\n"; my $content = ; close( I ); XMLdata::make_text( $content ); } else { my $content = $parser->parsefile( $file ); die "Invalid include file.\n" unless defined $content; my $root = XMLdata::root( $content ); die "Invalid include file.\n" unless defined $root; warn "Include file root not an inclusion element.\n" unless 'inclusion' eq $root->tagname(); expand_includes( $root ); $root->get_children(); } } #--------------------------------------------------------------- # Walk the supplied container element, replacing all include # elements with their contents. # # Input: $contain - a container object in which we will expand # include elements sub expand_includes { my $contain = shift; $contain->{Kids} = [ map { 'include' eq $_->tagname() ? expand_an_include( $_ ) : ($_ ) } @{$contain->{Kids}} ]; } #---------------------------------------------------------------- # Check the test set for validity. # # Input: $testset - reference to a testset object # # Output: false if valid, a string describing the problem if not sub invalid_testset { my $testset = shift; my %LegalTags = map { $_ => 1 } qw/testset param include option test codelib request response status header content code body time msg not and or session/; return "Invalid root element." unless 'testset' eq $testset->tagname(); my $is_valid = sub { my $el = shift; die $el->tagname()."\n" unless !$el->is_element() or $LegalTags{$el->tagname()}; }; eval { XMLdata::visit_elements( $testset, $is_valid ); }; if($@) { chomp( $@ ); return "Unrecognized element type \'$@\'"; } else { return 0; } } #--------------------------------------------------- # Open the logging file, if necessary sub open_logfile { if($Options{logging}) { open( LOG, ">>$Options{logfile}" ) or die "Unable to open log file '$Options{logfile}'.\n"; } } #--------------------------------------------------- # Close the logging file, if necessary sub close_logfile { close( LOG ) if $Options{logging}; } #--------------------------------------------------- # Print normal messages and add to log. # # Input: A list of messages to print sub printlog { print @_; print LOG @_ if $Options{logging}; } #--------------------------------------------------- # Print normal verbose messages and add to log. # # Input: A list of messages to print sub printlogv { print @_ if $Options{msgs} ne 'quiet'; print LOG @_ if $Options{logging}; } #--------------------------------------------------- # Print extra verbose messages and add to log. # # Input: A list of messages to print sub printlogvv { print @_ if 'verbose' eq $Options{msgs}; print LOG @_ if $Options{logging}; } #-------------------------------------------------- # Usage message sub usage { die <<"EOU"; Usage: $0 [-vwqVl] [-o logfile] [-I incpath] [-f list] [name=value ...] test.files ... Where: -v verbose output -w normal output, default -q quiet output -V print version information -l log results to HTTPtest.log -o send results to specified logfile -I semicolon-separated path to check for include files -f supply a file listing the test files to use name=value pairs override parameters in the test files test.files is a list of files that contain the tests to run EOU } #--------------------------------------------------------------------- # Code merged from other libraries to make standalone script version. #--------------------------------------------------------------------- { package XMLdata; use strict; use vars qw/$xlf $Pkg %convert/; $Pkg = 'obj'; #------------------------------------------------------------------- # For character conversions %convert = ( '&'=>'&', '<'=>'<', '>'=>'>', '"'=>'"' ); $xlf = make_text( "\n" ); # used for formatting #-------------------------------------------------------------------- # Set the current package name and setup the Characters class # # Input: $pkg - package name to use sub set_package { $Pkg = shift; } #--------------------------------------------------------------- # Create an XMLdata character string # # Input: a character string # # Return: a Characters object containing the text sub make_text { my $str = shift; bless { Text=>$str }, $Pkg."::Characters"; } #---------------------------------------------------------------- # Extract the root element from the reference returned by the # Object style of XML::Parser # # Input: return value from XML::Parser->parse() # # Return: the xml object representing the root element of the # XML document sub root { (grep { (ref $_) !~ /::Characters$/ } @{$_[0]})[0]; } #----------------------------------------------------------------- # Execute the supplied function on the supplied object and all of # its children. # # Input: $obj - an xml object # $exec - a code reference to execute on $obj and all of # its descendents sub visit_elements { my $obj = shift; my $exec = shift; $exec->( $obj, @_ ); return unless $obj->is_element(); foreach($obj->get_children()) { visit_elements( $_, $exec, @_ ); } } #----------------------------------------------------------------- # Make package associated with supplied element name a derived # class of XMLdata::Element # # Inputs: $name - name of an element class to derive from # XMLdata::Element sub DeclareElement { no strict "refs"; my $name = shift; $name = $XMLdata::Pkg.qq{::$name} unless $name =~ /::/; push @{$name.'::ISA'}, "XMLdata::Element"; } #----------------------------------------------------------------- # Make packages associated with supplied element names derived # classes of XMLdata::Element # # Inputs: @_ - list of names of element classes to derive from # XMLdata::Element sub DeclareElements { foreach (@_) { DeclareElement( $_ ); } } } { package tst::Characters; use strict; #--------------------------------------------------------------- # Return a recognizable string for character data # # Return: tag name as a string sub tagname { '#PCDATA'; } #---------------------------------------------------------------- # Return the text contained in a ::Characters node. # # Input: an xml Characters object sub get_text { $_[0]->{Text}; } #---------------------------------------------------------------- # Return a boolean stating that this is not an Element sub is_element { 0; } #---------------------------------------------------------------- # Returns true if the supplied object is a text object # # Input: an xml object # # Return: true if the xml object is a text object, false otherwise sub is_text { 1; } } { package XMLdata::Element; use strict; #--------------------------------------------------------------- # Return the tag name from an xml object (w/o the package) # # Return: tag name as a string sub tagname { my $self = shift; my $tag = ref $self; $tag =~ /([^:]+)$/; $1; } #---------------------------------------------------------------- # Returns the number of children in the element # # Return: the count of the number of children of the element sub count_children { my $self = shift; defined $self->{Kids} ? scalar @{$self->{Kids}} : 0; } #---------------------------------------------------------------- # Returns true if the object has no children. # # Return: true if the element has no children, false otherwise sub is_empty { my $self = shift; (0 == $self->count_children()); } #--------------------------------------------------------------- # Return a list (or a reference to a list) of children for the # object. # # Return: a list (or reference to a list in scalar context) of # xml objects sub get_children { my $self = shift; if(exists $self->{Kids}) { wantarray ? @{$self->{Kids}} : $self->{Kids}; } else { wantarray ? () : []; } } #--------------------------------------------------------------- # Return a list (or a reference to a list) of children of the # given type from the object. # # Input: $tag - a tag name # # Return: a list (or reference to a list in scalar context) of # xml objects sub get_children_by_tag { my $self = shift; my $tag = shift; $tag = $XMLdata::Pkg."::$tag" unless $tag =~ /::/; my @children = grep { $tag eq ref $_ } $self->get_children(); wantarray ? @children : \@children; } #--------------------------------------------------------------- # Return a list (or a reference to a list) of descendents of the # given type from the object. # # Input: an xml object # $tag - a tag name # # Return: a list (or reference to a list in scalar context) of # xml objects sub get_descendents_by_tag { my $self = shift; my $tag = shift; $tag = $XMLdata::Pkg."::$tag" unless $tag =~ /::/; my @descend = (); foreach my $child ($self->get_children()) { push @descend, $child if $tag eq ref $child; push @descend, $child->get_descendents_by_tag( $tag ) unless !$child->isa('XMLdata::Element') or $child->is_empty(); } wantarray ? @descend : \@descend; } #---------------------------------------------------------------- # Return a particular child by index of the object. # # Input: $n - an optional index # # Return: an xml objects sub get_child { my $self = shift; my $n = shift || 0; $self->get_children()->[$n]; } #---------------------------------------------------------------- # Return a particular child of the given type by index of the # object. # # Input: $tag - a tag name # $n - an optional index # # Return: an xml objects sub get_child_by_tag { my $self = shift; my $tag = shift; my $n = shift || 0; $self->get_children_by_tag( $tag )->[$n]; } #---------------------------------------------------------------- # Return the text contained in a given element # # Input: an xml object with containing text # # Return: the text string from the content sub get_text_content { $_[0]->get_child( 0 )->get_text(); } #---------------------------------------------------------------- # Return a boolean stating that this is an Element sub is_element { 1; } #---------------------------------------------------------------- # Returns true if the supplied object is a text object # # Input: an xml object # # Return: true if the xml object is a text object, false otherwise sub is_text { 0; } } __END__ =head1 NAME HTTPtestS.pl =head1 Purpose B is a generalized HTTP request test program. It allows for efficient regressions testing of any HTTP-based server. B is a version of the script modified to run standalone. Anywhere the name B is used in the documentation, B can be used as well. B This program is under construction, so the documentation is subject to change. =head1 SCRIPT CATEGORIES HTTP - suggested =head1 PREREQUISITES This script depends on both the C and C pragmas. The script also uses the C C, C, C, C, and C modules. =head1 COREQUISITES If the C module is available, it is used to generate higher-resolution timings on the time test criterion and the script timing. =head1 OSNAMES any. Tested on MSWin32 and linux. =head1 Description The B program uses a testset file to describe a series of tests to perform using the HTTP protocol. In general, a set of tests are aimed at a single server or service. Like HTTP itself, the tests are defined in terms of a request and a response. The testset file describes the tests in an XML-based format, described below. B reads each test in the testset file, performs the specified request, and evaluates the response based on the criteria described in the test. If all criteria are met, the test is successful. Otherwise, the test fails. =head1 Usage B is executed as follows: perl HTTPtest.pl [-vwqVl] [-o logfile] [-I incpath] [-f list] [name=value ...] test.files ... Where: -v verbose output -w normal output, default -q quiet output -V print version information -l log results to HTTPtest.log -o send results to specified logfile -I semicolon-separated path to check for include files -f supply a file listing the test files to use name=value pairs override parameters in the test files test.files is a list of files that contain the tests to run If run with a bad option or no test files, the above message is printed. =head1 Testset File The testset file contains a C root element. The C may contain C elements to include information from other files, C elements which factor out reused parameters, and C elements which describe the tests to run. Each C consist of a C, which describes the HTTP request, and a C which describes the criteria for evaluating the HTTP response. A C specifies the URL to call including any query parameters, an HTTP method such as I or I, and an optional request body. A C contains a series of evaluation criteria which include the ability to test: =over 4 =item status The response status code for the response. Reasonable values include I<200> and I<404>. =item header Tests for the existence or value of particular MIME-headers in the response. Useful for verifying redirections and testing returned cookies. =item content Evaluation criteria for the content of response. =item time Compare the response time of the URL request against a minimum and maximum timespan in seconds. =item code Execute arbitrary Perl code on the response to evaluate the response. =item msg This pseudo-criterion displays a message to the user and optionally aborts the current testset or program run. =back By default all of the evaluations in the C must succeed in order for the C to succeed. However, three other elements are supported to allow for arbitrary logical combinations of the evaluations. =over 4 =item and All of the evaluation criteria contained in the C element must succeed in order for the C to succeed. Any unsuccessful evaluation causes the C to fail without executing any remaining children. =item or One of the evaluation criteria contained in the C element must succeed in order for the C to succeed. Any successful evaluation causes the C to succeed without executing any remaining children. =item not The sense of the evaluation criterion contained in the C element is reversed, so that a failure becomes a success. =back =head1 Element List =head2 and The C element is used to combine a set of evaluation criteria in a logical I relationship. The C element can contain any of the evaluation elements or the logical elements. The C element has one optional attribute I