File Coverage

blib/lib/APP/REST/RestTestSuite.pm
Criterion Covered Total %
statement 28 30 93.3
branch n/a
condition n/a
subroutine 10 10 100.0
pod n/a
total 38 40 95.0


line stmt bran cond sub pod time code
1             package APP::REST::RestTestSuite;
2              
3 1     1   12271 use 5.006;
  1         2  
  1         26  
4 1     1   8 use strict;
  1         0  
  1         28  
5 1     1   3 use warnings FATAL => 'all';
  1         4  
  1         31  
6 1     1   452 use Data::Dumper;
  1         5808  
  1         55  
7 1     1   352 use HTTP::Request;
  1         12371  
  1         26  
8 1     1   376 use Time::HiRes qw( time sleep );
  1         1009  
  1         3  
9 1     1   121 use File::Path;
  1         1  
  1         39  
10 1     1   4 use Cwd;
  1         1  
  1         35  
11 1     1   460 use LWP::UserAgent;
  1         12833  
  1         19  
12 1     1   341 use APP::REST::ParallelMyUA;
  0            
  0            
13              
14              
15             use constant LOG_FILE => 'rest_client.log';
16             use constant ERR_LOG_FILE => 'rest_client_error.log';
17             use constant LINE => '=' x 50;
18              
19             $| = 1; #make the pipe hot
20             $Data::Dumper::Indent = 1;
21              
22             =head1 NAME
23              
24             APP::REST::RestTestSuite - Suite for testing restful web services
25              
26             =head1 VERSION
27              
28             Version 0.03
29              
30             =cut
31              
32             our $VERSION = '0.03';
33              
34             =head1 SYNOPSIS
35              
36             use APP::REST::RestTestSuite;
37             my $suite = APP::REST::RestTestSuite->new();
38              
39             $suite->execute_test_cases( $suite->get_test_cases() );
40             my ( $cases_in_config, $executed, $skipped, $passed, $failed ) =
41             $suite->get_result_summary();
42              
43             #OR
44              
45             use APP::REST::RestTestSuite;
46              
47             # overrides the default config and log file paths
48             my $suite = APP::REST::RestTestSuite->new(
49             REST_CONFIG_FILE => ,
50             LOG_FILE_PATH => ,
51             );
52              
53             $suite->execute_test_cases( $suite->get_test_cases() );
54             my ( $cases_in_config, $executed, $skipped, $passed, $failed ) =
55             $suite->get_result_summary();
56              
57            
58             =head1 DESCRIPTION
59              
60             APP::REST::RestTestSuite object is instantiated with the data in config file.
61             Default config file format is defined in __DATA__ and that can be overridden
62             by passing the config file as an argument to the class.
63             Default LOG file path is the current working directory of the script which
64             calls this module
65              
66             =head1 SUBROUTINES/METHODS
67              
68             =head2 new
69              
70             Object Constructor
71              
72             =cut
73              
74             sub new {
75             my ( $class, %args ) = @_;
76              
77             my $self = {};
78              
79             bless( $self, $class );
80              
81             $self->_init(%args);
82              
83             return $self;
84             }
85              
86             =head2 get_test_cases
87              
88              
89             =cut
90              
91             sub get_test_cases {
92             my ( $self, %args ) = @_;
93              
94             if ( $self->{test_cases} ) {
95             return %{ $self->{test_cases} };
96             } else {
97             return undef;
98             }
99             }
100              
101             =head2 get_log_file_handle
102              
103              
104             =cut
105              
106             sub get_log_file_handle {
107             my ( $self, %args ) = @_;
108              
109             return $self->{file}->{log_file_handle};
110             }
111              
112             =head2 get_err_log_file_handle
113              
114              
115             =cut
116              
117             sub get_err_log_file_handle {
118             my ( $self, %args ) = @_;
119              
120             return $self->{file}->{err_log_file_handle};
121             }
122              
123             =head2 get_config_file_handle
124              
125              
126             =cut
127              
128             sub get_config_file_handle {
129             my ( $self, %args ) = @_;
130              
131             return $self->{file}->{config_file_handle};
132             }
133              
134             =head2 get_config_file
135              
136              
137             =cut
138              
139             sub get_config_file {
140             my ( $self, %args ) = @_;
141              
142             return $self->{file}->{config_file};
143             }
144              
145             =head2 get_sample_config_file
146              
147              
148             =cut
149              
150             sub get_sample_config_file {
151             my ( $self, %args ) = @_;
152              
153             return $self->{file}->{sample_config_file};
154             }
155              
156             =head2 get_result_summary
157              
158              
159             =cut
160              
161             sub get_result_summary {
162             my ( $self, %args ) = @_;
163              
164             return (
165             $self->{test_result_log}->{test_cases_in_config},
166             $self->{test_result_log}->{test_cases_exececuted},
167             $self->{test_result_log}->{test_cases_skipped},
168             $self->{test_result_log}->{test_cases_passed},
169             $self->{test_result_log}->{test_cases_failed},
170             );
171             }
172              
173             =head2 validate_test_cases
174              
175              
176             =cut
177              
178             sub validate_test_cases {
179             my ($self) = shift;
180              
181             my $err = undef;
182              
183             unless (@_) {
184             $err = "There is no test cases defined to execute.\n";
185             } elsif ( ( (@_) % 2 ) == 1 ) {
186             $err =
187             "Test cases are not properly configured in '"
188             . $self->get_config_file()
189             . "'\nDefine test cases properly.\nPlease see the README file for more info.\n";
190             }
191             return $err if ($err);
192              
193             my %test_cases = @_;
194              
195             my @spec = sort qw(
196             test_case
197             uri
198             request_content_type
199             request_method
200             request_body
201             response_status
202             execute
203             response_content_type
204             );
205              
206             #below two are not mandatory for a test case as of now; if required add them to above array
207             # response_header
208             # response_body
209              
210             foreach my $count ( sort { $a <=> $b } keys(%test_cases) ) {
211              
212             my $tc = $test_cases{$count};
213             my @keys = sort keys %{$tc};
214              
215             no warnings;
216             $err .= "Test case '$tc->{test_case}' not properly defined\n"
217             unless ( _compare_arrays( \@spec, \@keys ) );
218             }
219              
220             $err .= "Please see the README file to see the correct format.\n" if ($err);
221              
222             return $err;
223              
224             }
225              
226             =head2 execute_test_cases
227              
228              
229             =cut
230              
231             sub execute_test_cases {
232             my ($self) = shift;
233              
234             #expects an hash with keys as test case number and value as hash ref with test
235             #specification; validate that before trying to execute them.
236             my $err = $self->validate_test_cases(@_);
237              
238             die "ERROR: $err\n" if ($err);
239              
240             my %test_cases = @_;
241              
242             my $ua = LWP::UserAgent->new;
243              
244             $ua->agent("RTAT/$VERSION");
245             $ua->timeout(90); # in seconds
246             $ua->default_header('Accept' => '*/*'); # to get cross platform support
247              
248              
249             my ( $config, $total, $total_response_time, $skip, $pass, $fail ) = (0) x 6;
250             my ( $uri, $method, $req_content_type, $req_body, $status ) = (undef) x 5;
251             my ( $request, $response ) = (undef) x 2;
252             my ( $username, $password ) = (undef) x 2;
253              
254             $username = $self->{username};
255             $password = $self->{password};
256              
257             my $fh = $self->get_log_file_handle();
258             my $err_fh = $self->get_err_log_file_handle();
259              
260             if ( $self->{html_log_required}
261             && ( $self->{html_log_required} =~ /yes/i ) )
262             {
263             print $fh
264             qq| LOG for $self->{endpoint} |
265             . qq||;
414             print $err_fh qq||;
415             }
416              
417             $self->{test_result_log} = {
418             test_cases_in_config => $config,
419             test_cases_exececuted => $total,
420             test_cases_skipped => $skip,
421             test_cases_passed => $pass,
422             test_cases_failed => $fail,
423             };
424              
425             close($fh);
426             close($err_fh);
427              
428             }
429              
430             =head2 execute_test_cases_in_parallel
431              
432              
433             =cut
434              
435             sub execute_test_cases_in_parallel {
436             my ($self) = shift;
437              
438             #Code expects an hash with keys as test case number and value as hash ref with test
439             #specification; validate that before trying to execute them.
440             my $err = $self->validate_test_cases(@_);
441              
442             die "ERROR: $err\n" if ($err);
443              
444             my %test_cases = @_;
445              
446             # use my customized user agent for parallel invokes
447             my $pua = APP::REST::ParallelMyUA->new();
448              
449             $pua->agent("RTAT/$VERSION");
450             $pua->in_order(1); # handle requests in order of registration
451             $pua->duplicates(0); # ignore duplicates
452             $pua->timeout(60); # in seconds
453             $pua->redirect(1); # follow redirects
454             $pua->default_header('Accept' => '*/*'); # to get cross platform support
455              
456             my ( $config, $total, $total_response_time, $skip, $pass, $fail ) = (0) x 6;
457             my ( $uri, $method, $req_content_type, $req_body, $status ) = (undef) x 5;
458             my ( $request, $response ) = (undef) x 2;
459             my ( $username, $password ) = (undef) x 2;
460              
461             $username = $self->{username};
462             $password = $self->{password};
463              
464             my $fh = $self->get_log_file_handle();
465              
466             if ( $self->{html_log_required}
467             && ( $self->{html_log_required} =~ /yes/i ) )
468             {
469             print $fh
470             qq| LOG for $self->{endpoint} |
471             . qq||;
573             }
574              
575             $self->{test_result_log} = {
576             test_cases_in_config => $config,
577             test_cases_exececuted => $total,
578             test_cases_skipped => $skip,
579             test_cases_passed => $pass,
580             test_cases_failed => $fail,
581             };
582              
583             close($fh);
584              
585             }
586              
587             =head2 get_sample_test_suite
588              
589              
590             =cut
591              
592             sub get_sample_test_suite {
593             my ( $self, %args ) = @_;
594              
595             $self->_init_sample_config_file();
596              
597             my $file = $self->{file};
598             my $wfh =
599             $self->_open_fh( FILE => $file->{sample_config_file}, MODE => 'WRITE' );
600              
601             foreach ( @{$file->{config_file_content}}) {
602             print $wfh $_;
603             }
604             close($wfh);
605             }
606              
607             =head2 delta_time
608              
609              
610             =cut
611              
612             sub delta_time {
613             my ( $self, %args ) = @_;
614              
615             my $now = time;
616             return ( ( $now - $args{start_time} ) * 1000 ); #convert to milli seconds
617             }
618              
619             sub _init {
620             my ( $self, %args ) = @_;
621              
622             $self->_init_config_file_handle(%args);
623              
624             # Read the config file based on the type of the input file (xml or text)
625             if ( $args{CONFIG_FILE_TYPE} && ( $args{CONFIG_FILE_TYPE} =~ /xml/i ) ) {
626              
627             #Implement the xml reading
628             } else {
629             $self->_init_read_config(%args);
630             }
631              
632             $self->_init_log_file_handle(%args);
633              
634             $self->_init_rest_base_uri(%args);
635             }
636              
637             sub _init_config_file_handle {
638             my ( $self, %args ) = @_;
639              
640             $self->_init_config_files(%args);
641              
642             my $file = $self->{file};
643             if ( $file->{config_file} ) {
644             $file->{config_file_handle} =
645             $self->_open_fh( FILE => $file->{config_file}, MODE => 'READ' );
646             } else {
647             $file->{config_file_handle} = \*APP::REST::RestTestSuite::DATA;
648             }
649              
650             $self->{file} = $file;
651             }
652              
653             sub _init_log_file_handle {
654             my ( $self, %args ) = @_;
655              
656             $self->_init_log_files(%args)
657             ; #Make compatible with windows and linux logging
658              
659             my $file = $self->{file};
660              
661             $file->{log_file_handle} =
662             $self->_open_fh( FILE => $file->{log_file}, MODE => 'WRITE' );
663             $file->{err_log_file_handle} =
664             $self->_open_fh( FILE => $file->{err_log_file}, MODE => 'WRITE' );
665              
666             $self->{file} = $file;
667             }
668              
669             sub _init_read_config {
670             my ( $self, %args ) = @_;
671              
672             my $fh = $self->get_config_file_handle();
673              
674             my @buffer = ();
675             my @start_end_buffer = ();
676              
677             my ( $start_case, $end_case, $test_no ) = (undef) x 3;
678             my ( $start_common, $end_common ) = (undef) x 2;
679             my ( $start_http_code, $end_http_code ) = (undef) x 2;
680             my ( $start_tag, $lines_between, $end_tag ) = (undef) x 3;
681              
682             my $separator = ":"; # separator used in config file for key/value pair
683             my %start_end_hash;
684              
685             my $file = $self->{file};
686              
687             while (<$fh>) {
688              
689             push (@{$file->{config_file_content}} , $_);
690             _trim( chomp($_) );
691             next if ( $_ =~ m/^#+$/ || $_ =~ m/^\s*$/ || $_ =~ m/^#\s+/ );
692             last if ( $_ =~ m/^#END_OF_CONFIG_FILE\s*$/ );
693              
694             ## Process common configuration for all test cases
695             if ( $_ =~ m/^#START_COMMON_CONFIG\s*$/ ) {
696             $start_common = 1;
697             next;
698             }
699             $end_common = 1 if ( $_ =~ m/^#END_COMMON_CONFIG\s*$/ );
700             push( @buffer, $_ ) if ( $start_common && !$end_common );
701              
702             if ( $start_common && $end_common ) {
703             foreach my $line (@buffer) {
704             my @val = split( $separator, $line );
705             $self->{ _trim( $val[0] ) } = _trim( $val[1] );
706             }
707              
708             @buffer = ();
709             $start_common = 0;
710             $end_common = 0;
711             } elsif ( !$start_common && $end_common ) {
712             die "ERROR in config file format\n";
713             }
714              
715             ## Process test cases
716             if ( $_ =~ m/^#START_TEST_CASE\s*$/ ) {
717             $start_case = 1;
718             next;
719             }
720             $end_case = 1 if ( $_ =~ m/^#END_TEST_CASE\s*$/ );
721              
722             push( @buffer, $_ ) if ( $start_case && !$end_case );
723              
724             ## Process [START] and [END] tag for any keys
725             $start_tag = 1 if ( $_ =~ m/^\s*\[START\]\s*$/ );
726             $end_tag = 1 if ( $_ =~ m/^\s*\[END\]\s*$/ );
727              
728             if ( $start_tag && !$end_tag ) {
729             $lines_between++;
730             push( @start_end_buffer, $_ );
731             }
732              
733             if ( $start_tag && $end_tag ) {
734              
735             my $req_body = '';
736             $req_body .= _trim($_) foreach (@start_end_buffer);
737             $req_body =~ s/\[START\]//g;
738             $req_body =~ s/\[END\]//g;
739              
740             while ( $lines_between >= 0 ) {
741             $lines_between--;
742             pop @buffer;
743             }
744              
745             my $line = pop @buffer;
746             $line =~ s/\s+://g;
747             $line =~ s/^\s+//g;
748              
749             ## create the key with the values given in between [START] and [END] tag
750             $start_end_hash{$line} = $req_body;
751              
752             @start_end_buffer = ();
753             $start_tag = 0;
754             $end_tag = 0;
755             $lines_between = 0;
756             }
757              
758             if ( $start_case && $end_case ) {
759             $test_no++;
760             foreach my $line (@buffer) {
761             my @val = split( $separator, $line );
762             $self->{test_cases}->{$test_no}->{ _trim( $val[0] ) } =
763             _trim( $val[1] );
764             }
765              
766             # add all those in between [START] and [END] tage to the respective key
767             while ( my ( $key, $value ) = each %start_end_hash ) {
768             $self->{test_cases}->{$test_no}->{$key} = $value;
769             }
770              
771             %start_end_hash = ();
772             @buffer = ();
773             $start_case = 0;
774             $end_case = 0;
775             } elsif ( !$start_case && $end_case ) {
776             die "ERROR in config file format\n";
777             }
778              
779             ## Process HTTP status codes
780             if ( $_ =~ m/^#START_HTTP_CODE_DEF\s*$/ ) {
781             $start_http_code = 1;
782             next;
783             }
784             $end_http_code = 1 if ( $_ =~ m/^#END_HTTP_CODE_DEF\s*$/ );
785             push( @buffer, $_ ) if ( $start_http_code && !$end_http_code );
786              
787             if ( $start_http_code && $end_http_code ) {
788             foreach my $line (@buffer) {
789             my @val = split( $separator, $line );
790             $self->{http_status_code}->{ _trim( $val[0] ) } =
791             _trim( $val[1] );
792             }
793              
794             @buffer = ();
795             $start_http_code = 0;
796             $end_http_code = 0;
797             } elsif ( !$start_http_code && $end_http_code ) {
798             die "ERROR in config file format\n";
799             }
800              
801             }
802             close($fh);
803             }
804              
805             sub _init_rest_base_uri {
806             my ( $self, %args ) = @_;
807              
808             if ( $self->{username} ) {
809             print STDERR "username configured: $self->{username}\n";
810             print STDERR "Password: ";
811             chomp( $self->{password} = );
812             }
813              
814             if ( $self->{endpoint} && $self->{port} && $self->{base_uri} ) {
815             $self->{rest_uri_base} =
816             qq|http://$self->{endpoint}|
817             . qq|:$self->{port}|
818             . qq|$self->{base_uri}|;
819             return; #use the port and uri in config file and return from sub
820             } elsif ( $self->{endpoint} && $self->{base_uri} ) {
821             $self->{rest_uri_base} =
822             qq|http://$self->{endpoint}| . qq|$self->{base_uri}|;
823             return; #use the uri in config file and return from sub
824             } elsif ( $self->{endpoint} && $self->{port} ) {
825             $self->{rest_uri_base} =
826             qq|http://$self->{endpoint}| . qq|:$self->{port}|;
827             return;
828             } elsif ( $self->{endpoint} ) {
829             $self->{rest_uri_base} = qq|http://$self->{endpoint}|;
830             return; #use the endpoint in config file and return from sub
831             } else {
832             die qq|Endpoint should be configured in the config file\n|;
833             }
834              
835             }
836              
837             sub _init_config_files {
838             my ( $self, %args ) = @_;
839              
840             $self->{file}->{config_file} = $args{REST_CONFIG_FILE};
841             }
842              
843             sub _init_sample_config_file {
844             my ( $self, %args ) = @_;
845              
846             my $separator;
847             if ( $^O =~ /Win/ ) {
848             $separator = '\\';
849             } else {
850             $separator = '/';
851             }
852              
853             my $scfg = getcwd() || $ENV{PWD};
854             my $scfg_file = $scfg . $separator . 'rest-project-xxxx.txt';
855              
856             $self->{file}->{sample_config_file} = $scfg_file;
857              
858             }
859              
860             sub _init_log_files {
861             my ( $self, %args ) = @_;
862              
863             my $separator;
864             if ( $^O =~ /Win/ ) {
865             $separator = '\\';
866             } else {
867             $separator = '/';
868             }
869              
870             my $log_path = $args{LOG_FILE_PATH} || getcwd() || $ENV{PWD};
871             my $log_dir = $log_path . $separator . 'LOG';
872              
873             eval { mkpath( $log_dir, 0, 0755 ) unless ( -d $log_dir ); };
874              
875             if ($@) {
876             my $err = $@;
877             $err =~ s/line\s+\d+//g;
878             die qq|Unable to create LOG directory ERROR: $err\n|;
879             }
880              
881             my $log_file = join(
882             $separator,
883             (
884             $log_dir,
885             (
886             $self->{html_log_required}
887             && ( $self->{html_log_required} =~ /yes/i )
888             )
889             ? LOG_FILE
890             . ".html"
891             : LOG_FILE
892             )
893             );
894             my $error_log_file = join(
895             $separator,
896             (
897             $log_dir,
898             (
899             $self->{html_log_required}
900             && ( $self->{html_log_required} =~ /yes/i )
901             )
902             ? ERR_LOG_FILE
903             . ".html"
904             : ERR_LOG_FILE
905             )
906             );
907             $self->{file}->{log_file} = $log_file;
908             $self->{file}->{err_log_file} = $error_log_file;
909             }
910              
911             sub _open_fh {
912             my ( $self, %args ) = @_;
913              
914             my ( $fh, $err ) = (undef) x 2;
915              
916             my $file = $args{FILE};
917             my $mode = $args{MODE};
918              
919             if ( $mode =~ m/READ/i ) {
920             open( $fh, '<', "$file" ) or ( $err = 'yes' );
921             } elsif ( $mode =~ m/WRITE/i ) {
922             open( $fh, '>', "$file" ) or ( $err = 'yes' );
923             } elsif ( $mode =~ m/APPEND/i ) {
924             open( $fh, '>>', "$file" ) or ( $err = 'yes' );
925             }
926              
927             if ($err) {
928             die qq|\nUnable to open file '$file' for $mode\nERROR: $!\n|;
929             }
930              
931             return $fh;
932              
933             }
934              
935             sub _trim($) {
936              
937             return unless ( $_[0] );
938              
939             my $str = $_[0];
940             $str =~ s/^\s+//g;
941             $str =~ s/\s+$//g;
942              
943             return $str;
944             }
945              
946             sub _print_logs {
947             my ( $self, %args ) = @_;
948              
949             no warnings;
950              
951             my $fh = $args{fh};
952             my $res = $args{res};
953             my $uri = $args{uri};
954             my $method = $args{method};
955             my $req_body = $args{req_body};
956              
957             my $define =
958             "Definition of status code not available; Please define in config if this is a custom code";
959              
960             unless ( $args{res} ) {
961             print $fh "\n";
962             print $fh "URI => $uri\n";
963             print $fh "HTTP Method => $method\n";
964             print $fh "Request Body => \n$req_body\n" if ( $method !~ /get/i );
965             } else {
966              
967             print $fh "\n";
968             print $fh "Response code => ";
969             print $fh $res->code;
970             print $fh " [ ";
971             print $fh ( exists $self->{http_status_code}->{ $res->code } )
972             ? $self->{http_status_code}->{ $res->code }
973             : $define;
974             print $fh " ]\n";
975             print $fh "\n\nResponse Content =>\n";
976             print $fh $res->content;
977             print $fh "\n\nTest execution time => ";
978             print $fh $args{exec_time};
979             print $fh " milli seconds";
980             print $fh "\n", LINE, "\n";
981             }
982              
983             }
984              
985             sub _compare_arrays {
986             my ( $first, $second ) = @_;
987             no warnings; # silence spurious -w undef complaints
988             return 0 unless @$first == @$second;
989             for ( my $i = 0 ; $i < @$first ; $i++ ) {
990             return 0 if $first->[$i] ne $second->[$i];
991             }
992             return 1;
993             }
994              
995             =head1 AUTHOR
996              
997             Mithun Radhakrishnan, C<< >>
998              
999             =head1 BUGS
1000              
1001              
1002             =head1 SUPPORT
1003              
1004             You can find documentation for this module with the perldoc command.
1005              
1006             perldoc APP::REST::RestTestSuite
1007              
1008              
1009             =head1 ACKNOWLEDGEMENTS
1010              
1011              
1012             =head1 LICENSE AND COPYRIGHT
1013              
1014             Copyright 2014 Mithun Radhakrishnan.
1015              
1016             This program is free software; you can redistribute it and/or modify it
1017             under the terms of the the Artistic License (2.0). You may obtain a
1018             copy of the full license at:
1019              
1020             L
1021              
1022             Any use, modification, and distribution of the Standard or Modified
1023             Versions is governed by this Artistic License. By using, modifying or
1024             distributing the Package, you accept this license. Do not use, modify,
1025             or distribute the Package, if you do not accept this license.
1026              
1027             If your Modified Version has been derived from a Modified Version made
1028             by someone other than you, you are nevertheless required to ensure that
1029             your Modified Version complies with the requirements of this license.
1030              
1031             This license does not grant you the right to use any trademark, service
1032             mark, tradename, or logo of the Copyright Holder.
1033              
1034             This license includes the non-exclusive, worldwide, free-of-charge
1035             patent license to make, have made, use, offer to sell, sell, import and
1036             otherwise transfer the Package with respect to any patent claims
1037             licensable by the Copyright Holder that are necessarily infringed by the
1038             Package. If you institute patent litigation (including a cross-claim or
1039             counterclaim) against any party alleging that the Package constitutes
1040             direct or contributory patent infringement, then this Artistic License
1041             to you shall terminate on the date that such litigation is filed.
1042              
1043             Disclaimer of Warranty: THE PACKAGE IS PROVIDED BY THE COPYRIGHT HOLDER
1044             AND CONTRIBUTORS "AS IS' AND WITHOUT ANY EXPRESS OR IMPLIED WARRANTIES.
1045             THE IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
1046             PURPOSE, OR NON-INFRINGEMENT ARE DISCLAIMED TO THE EXTENT PERMITTED BY
1047             YOUR LOCAL LAW. UNLESS REQUIRED BY LAW, NO COPYRIGHT HOLDER OR
1048             CONTRIBUTOR WILL BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, OR
1049             CONSEQUENTIAL DAMAGES ARISING IN ANY WAY OUT OF THE USE OF THE PACKAGE,
1050             EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
1051              
1052              
1053             =cut
1054              
1055             =head1 REPOSITORY
1056              
1057             L
1058              
1059             =cut
1060              
1061             1; # End of APP::REST::RestTestSuite
1062              
1063             __DATA__