Allow to skip tests that depend on unavailable options.
[privoxy.git] / tools / privoxy-regression-test.pl
1 #!/usr/bin/perl
2
3 ############################################################################
4 #
5 # Privoxy-Regression-Test
6 #
7 # A regression test "framework" for Privoxy. For documentation see:
8 # perldoc privoxy-regression-test.pl
9 #
10 # $Id: privoxy-regression-test.pl,v 1.109 2008/02/15 16:40:04 fk Exp fk $
11 #
12 # Wish list:
13 #
14 # - Update documentation
15 # - Validate HTTP times.
16 # - Understand default.action.master comment syntax
17 #   and verify that we actually block and unblock what
18 #   the comments claim we do.
19 # - Implement a HTTP_VERSION directive or allow to
20 #   specify whole request lines.
21 # - Support filter regression tests.
22 # - Add option to fork regression tests and run them in parallel,
23 #   possibly optional forever.
24 # - Document magic Expect Header values
25 # - Internal fuzz support?
26 #
27 # Copyright (c) 2007-2008 Fabian Keil <fk@fabiankeil.de>
28 #
29 # Permission to use, copy, modify, and distribute this software for any
30 # purpose with or without fee is hereby granted, provided that the above
31 # copyright notice and this permission notice appear in all copies.
32 #
33 # THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
34 # WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
35 # MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
36 # ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
37 # WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
38 # ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
39 # OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
40 #
41 ############################################################################
42
43 use warnings;
44 use strict;
45 use Getopt::Long;
46
47 use constant {
48                PRT_VERSION => 'Privoxy-Regression-Test 0.2',
49  
50                CURL => 'curl',
51
52                # CLI option defaults
53                CLI_RETRIES  => 1,
54                CLI_LOOPS    => 1,
55                CLI_MAX_TIME => 5,
56                CLI_MIN_LEVEL => 0,
57                CLI_MAX_LEVEL => 25,
58
59                PRIVOXY_CGI_URL => 'http://p.p/',
60                FELLATIO_URL    => 'http://10.0.0.1:8080/',
61                LEADING_LOG_DATE => 1,
62                LEADING_LOG_TIME => 1,
63
64                DEBUG_LEVEL_FILE_LOADING    => 0,
65                DEBUG_LEVEL_PAGE_FETCHING   => 0,
66
67                VERBOSE_TEST_DESCRIPTION    => 1,
68
69                DEBUG_LEVEL_VERBOSE_FAILURE => 1,
70                # XXX: Only partly implemented and mostly useless.
71                DEBUG_LEVEL_VERBOSE_SUCCESS => 0,
72                DEBUG_LEVEL_STATUS          => 1,
73
74                # Internal use, don't modify
75                # Available debug bits:
76                LL_ERROR                   =>  1,
77                LL_VERBOSE_FAILURE         =>  2,
78                LL_PAGE_FETCHING           =>  4,
79                LL_FILE_LOADING            =>  8,
80                LL_VERBOSE_SUCCESS         => 16,
81                LL_STATUS                  => 32,
82                LL_SOFT_ERROR              => 64,
83
84                CLIENT_HEADER_TEST         =>  1,
85                SERVER_HEADER_TEST         =>  2,
86                DUMB_FETCH_TEST            =>  3,
87                METHOD_TEST                =>  4,
88                TRUSTED_CGI_REQUEST        =>  6,
89 };
90
91 sub init_our_variables () {
92
93     our $leading_log_time = LEADING_LOG_TIME;
94     our $leading_log_date = LEADING_LOG_DATE;
95
96     our $privoxy_cgi_url  = PRIVOXY_CGI_URL;
97
98     our $verbose_test_description = VERBOSE_TEST_DESCRIPTION;
99
100     our $log_level = get_default_log_level();
101
102 }
103
104 sub get_default_log_level () {
105     
106     my $log_level = 0;
107
108     $log_level |= LL_FILE_LOADING    if DEBUG_LEVEL_FILE_LOADING;
109     $log_level |= LL_PAGE_FETCHING   if DEBUG_LEVEL_PAGE_FETCHING;
110     $log_level |= LL_VERBOSE_FAILURE if DEBUG_LEVEL_VERBOSE_FAILURE;
111     $log_level |= LL_VERBOSE_SUCCESS if DEBUG_LEVEL_VERBOSE_SUCCESS;
112     $log_level |= LL_STATUS          if DEBUG_LEVEL_STATUS;
113
114     # These are intended to be always on.
115     $log_level |= LL_SOFT_ERROR;
116     $log_level |= LL_ERROR;
117
118     return $log_level;
119 }
120
121 ############################################################################
122 #
123 # File loading functions
124 #
125 ############################################################################
126
127 sub parse_tag ($) {
128
129     my $tag = shift;
130
131     # Remove anchors
132     $tag =~ s@[\$\^]@@g;
133     # Unescape brackets and dots
134     $tag =~ s@\\(?=[{}().+])@@g;
135
136     # log_message("Parsed tag: " . $tag);
137
138     check_for_forbidden_characters($tag);
139
140     return $tag;
141 }
142
143 sub check_for_forbidden_characters ($) {
144
145     my $tag = shift; # XXX: also used to check values though.
146     my $allowed = '[-=\dA-Za-z{}:.\/();\s,+@"_%\?&]';
147
148     unless ($tag =~ m/^$allowed*$/) {
149         my $forbidden = $tag;
150         $forbidden =~ s@^$allowed*(.).*@$1@;
151
152         l(LL_ERROR, "'" . $tag . "' contains character '" . $forbidden. "' which is unacceptable.");
153     }
154 }
155
156 sub load_regressions_tests () {
157
158     our $privoxy_cgi_url;
159     our @privoxy_config;
160     my @actionfiles;
161     my $curl_url = '';
162     my $file_number = 0;
163
164     $curl_url .= $privoxy_cgi_url;
165     $curl_url .= 'show-status';
166
167     l(LL_STATUS, "Asking Privoxy for the number of action files available ...");
168
169     foreach (@{get_cgi_page_or_else($curl_url)}) {
170
171         chomp;
172         if (/<td>(.*?)<\/td><td class=\"buttons\"><a href=\"\/show-status\?file=actions&amp;index=(\d+)\">/) {
173
174             my $url = $privoxy_cgi_url . 'show-status?file=actions&index=' . $2;
175             $actionfiles[$file_number++] = $url;
176
177         } elsif (m@config\.html#.*\">([^<]*)</a>\s+(.*)<br>@) {
178
179             my $directive = $1 . " " . $2;
180             push (@privoxy_config, $directive);
181         }
182     }
183
184     l(LL_FILE_LOADING, "Recognized " . @actionfiles . " actions files");
185
186     load_action_files(\@actionfiles);
187 }
188
189 sub token_starts_new_test ($) {
190
191     my $token = shift;
192     my @new_test_directives =
193         ('set header', 'fetch test', 'trusted cgi request', 'request header', 'method test');
194
195     foreach my $new_test_directive (@new_test_directives) {
196         return 1 if $new_test_directive eq $token;
197     }
198     return 0;
199
200 }
201
202 sub tokenize ($) {
203
204     my ($token, $value) = (undef, undef);
205
206     # Remove leading and trailing white space.
207     s@^\s*@@;
208     s@\s*$@@;
209
210     # Reverse HTML-encoding
211     # XXX: Seriously imcomplete. 
212     s@&quot;@"@g;
213
214     # Tokenize
215     if (/^\#\s*([^=]*?)\s*[=]\s*(.*?)\s*$/) {
216
217         $token = $1;
218         $token =~ tr/[A-Z]/[a-z]/;
219         $value = $2;
220
221     } elsif (/^TAG\s*:(.*)$/) {
222
223         $token = 'tag';
224         $value = $1;
225
226     }
227
228     return ($token, $value);
229 }
230
231 sub enlist_new_test ($$$$$$) {
232
233     my ($regression_tests, $token, $value, $si, $ri, $number) = @_;
234
235     if ($token eq 'set header') {
236
237         l(LL_FILE_LOADING, "Header to set: " . $value);
238         ${$regression_tests}[$si][$ri]{'type'} = CLIENT_HEADER_TEST;
239         # Implicit default
240         $$regression_tests[$si][$ri]{'level'} = CLIENT_HEADER_TEST;
241
242     } elsif ($token eq 'request header') {
243
244         l(LL_FILE_LOADING, "Header to request: " . $value);
245         $$regression_tests[$si][$ri]{'type'} = SERVER_HEADER_TEST;
246         # Implicit default
247         $$regression_tests[$si][$ri]{'expected-status-code'} = 200;
248         $$regression_tests[$si][$ri]{'level'} = SERVER_HEADER_TEST;
249
250     } elsif ($token eq 'trusted cgi request') {
251
252         l(LL_FILE_LOADING, "CGI URL to test in a dumb way: " . $value);
253         $$regression_tests[$si][$ri]{'type'} = TRUSTED_CGI_REQUEST;
254         # Implicit default
255         $$regression_tests[$si][$ri]{'expected-status-code'} = 200;
256         $$regression_tests[$si][$ri]{'level'} = TRUSTED_CGI_REQUEST;
257
258     } elsif ($token eq 'fetch test') {
259
260         l(LL_FILE_LOADING, "URL to test in a dumb way: " . $value);
261         $$regression_tests[$si][$ri]{'type'} = DUMB_FETCH_TEST;
262         # Implicit default
263         $$regression_tests[$si][$ri]{'expected-status-code'} = 200;
264         $$regression_tests[$si][$ri]{'level'} = DUMB_FETCH_TEST;
265
266     } elsif ($token eq 'method test') {
267
268         l(LL_FILE_LOADING, "Method to test: " . $value);
269         $$regression_tests[$si][$ri]{'type'} = METHOD_TEST;
270         # Implicit default
271         $$regression_tests[$si][$ri]{'expected-status-code'} = 200;
272         $$regression_tests[$si][$ri]{'level'} = METHOD_TEST;
273
274     } else {
275
276         die "Incomplete '" . $token . "' support detected."; 
277
278     }
279
280     check_for_forbidden_characters($value);
281
282     $$regression_tests[$si][$ri]{'data'} = $value;
283
284     # For function that only get passed single tests
285     $$regression_tests[$si][$ri]{'section-id'} = $si;
286     $$regression_tests[$si][$ri]{'regression-test-id'} = $ri;
287     $$regression_tests[$si][$ri]{'number'} = $number - 1;
288     l(LL_FILE_LOADING,
289       "Regression test " . $number . " (section:" . $si . "):");
290 }
291
292 sub load_action_files ($) {
293
294     # initialized here
295     our %actions;
296     our @regression_tests;
297
298     my $actionfiles_ref = shift;
299     my @actionfiles = @{$actionfiles_ref};
300
301     my $si = 0;  # Section index
302     my $ri = -1; # Regression test index
303     my $count = 0;
304
305     my $ignored = 0;
306
307     l(LL_STATUS, "Loading regression tests from action file(s) delivered by Privoxy.");
308
309     for my $file_number (0 .. @actionfiles - 1) {
310
311         my $curl_url = ' "' . $actionfiles[$file_number] . '"';
312         my $actionfile = undef;
313
314         foreach (@{get_cgi_page_or_else($curl_url)}) {
315
316             my $no_checks = 0;
317             chomp;
318             
319             if (/<h2>Contents of Actions File (.*?)</) {
320                 $actionfile = $1;
321                 next;
322             }
323             next unless defined $actionfile;
324
325             last if (/<\/pre>/);
326
327             my ($token, $value) = tokenize($_);
328
329             next unless defined $token;
330
331             # Load regression tests
332
333             if (token_starts_new_test($token)) {
334
335                 # Beginning of new regression test.
336                 $ri++;
337                 $count++;
338                 enlist_new_test(\@regression_tests, $token, $value, $si, $ri, $count);
339             }
340
341             if ($token =~ /level\s+(\d+)/i) {
342
343                 my $level = $1;
344                 register_dependency($level, $value);
345             }
346             
347             if ($si == -1 || $ri == -1) {
348                 # No beginning of a test detected yet,
349                 # so we don't care about any other test
350                 # attributes.
351                 next;
352             }
353
354             if ($token eq 'expect header') {
355
356                 l(LL_FILE_LOADING, "Detected expectation: " . $value);
357                 $regression_tests[$si][$ri]{'expect-header'} = $value;
358
359             } elsif ($token eq 'tag') {
360                 
361                 next if ($ri == -1);
362
363                 my $tag = parse_tag($value);
364
365                 # We already checked in parse_tag() after filtering
366                 $no_checks = 1;
367
368                 l(LL_FILE_LOADING, "Detected TAG: " . $tag);
369
370                 # Save tag for all tests in this section
371                 do {
372                     $regression_tests[$si][$ri]{'tag'} = $tag; 
373                 } while ($ri-- > 0);
374
375                 $si++;
376                 $ri = -1;
377
378             } elsif ($token eq 'ignore' && $value =~ /Yes/i) {
379
380                 l(LL_FILE_LOADING, "Ignoring section: " . test_content_as_string($regression_tests[$si][$ri]));
381                 $regression_tests[$si][$ri]{'ignore'} = 1;
382                 $ignored++;
383
384             } elsif ($token eq 'expect status code') {
385
386                 l(LL_FILE_LOADING, "Expecting status code: " . $value);
387                 $regression_tests[$si][$ri]{'expected-status-code'} = $value;
388
389             } elsif ($token eq 'level') { # XXX: stupid name
390
391                 $value =~ s@(\d+).*@$1@;
392                 l(LL_FILE_LOADING, "Level: " . $value);
393                 $regression_tests[$si][$ri]{'level'} = $value;
394
395             } elsif ($token eq 'method') {
396
397                 l(LL_FILE_LOADING, "Method: " . $value);
398                 $regression_tests[$si][$ri]{'method'} = $value;
399
400             } else {
401
402                 # We don't use it, so we don't need
403                 $no_checks = 1;
404             }
405             # XXX: Neccessary?
406             check_for_forbidden_characters($value) unless $no_checks;
407             check_for_forbidden_characters($token);
408         }
409     }
410
411     l(LL_FILE_LOADING, "Done loading " . $count . " regression tests." 
412       . " Of which " . $ignored. " will be ignored)\n");
413 }
414
415 ############################################################################
416 #
417 # Regression test executing functions
418 #
419 ############################################################################
420
421 sub execute_regression_tests () {
422
423     our @regression_tests;
424     my $loops = get_cli_option('loops');
425     my $all_tests    = 0;
426     my $all_failures = 0;
427     my $all_successes = 0;
428
429     unless (@regression_tests) {
430
431         l(LL_STATUS, "No regression tests found.");
432         return;
433     }
434
435     l(LL_STATUS, "Executing regression tests ...");
436
437     while ($loops-- > 0) {
438
439         my $successes = 0;
440         my $tests = 0;
441         my $failures;
442         my $skipped = 0;
443
444         for my $s (0 .. @regression_tests - 1) {
445
446             my $r = 0;
447
448             while (defined $regression_tests[$s][$r]) {
449
450                 die "Section id mismatch" if ($s != $regression_tests[$s][$r]{'section-id'});
451                 die "Regression test id mismatch" if ($r != $regression_tests[$s][$r]{'regression-test-id'});
452
453                 my $number = $regression_tests[$s][$r]{'number'};
454
455                 if ($regression_tests[$s][$r]{'ignore'}
456                     or level_is_unacceptable($regression_tests[$s][$r]{'level'})
457                     or test_number_is_unacceptable($number)) {
458
459                     $skipped++;
460
461                 } else {
462
463                     my $result = execute_regression_test($regression_tests[$s][$r]);
464
465                     log_result($regression_tests[$s][$r], $result, $tests);
466
467                     $successes += $result;
468                     $tests++;
469                 }
470                 $r++;
471             }
472         }
473         $failures = $tests - $successes;
474
475         log_message("Executed " . $tests . " regression tests. " .
476             'Skipped ' . $skipped . '. ' . 
477             $successes . " successes, " . $failures . " failures.");
478
479         $all_tests    += $tests;
480         $all_failures += $failures;
481         $all_successes += $successes;
482
483     }
484
485     if (get_cli_option('loops') > 1) {
486         log_message("Total: Executed " . $all_tests . " regression tests. " .
487             $all_successes . " successes, " . $all_failures . " failures.");
488     }
489 }
490
491 sub level_is_unacceptable ($) {
492     my $level = shift;
493     return ((cli_option_is_set('level') and get_cli_option('level') != $level)
494             or ($level < get_cli_option('min-level'))
495             or ($level > get_cli_option('max-level'))
496             or dependency_unsatisfied($level)
497             );
498 }
499
500 sub test_number_is_unacceptable ($) {
501     my $test_number = shift;
502     return (cli_option_is_set('test-number')
503             and get_cli_option('test-number') != $test_number)
504 }
505
506 sub dependency_unsatisfied ($) {
507
508     my $level = shift;
509     our %dependencies;
510     our @privoxy_config;
511     my $dependency_problem = 0;
512
513     if (defined ($dependencies{$level}{'config line'})) {
514
515         my $dependency = $dependencies{$level}{'config line'};
516         $dependency_problem = 1;
517
518         foreach (@privoxy_config) {
519
520              $dependency_problem = 0 if (/$dependency/);
521         }
522     }
523
524     return $dependency_problem;
525 }
526
527 sub register_dependency ($$) {
528
529     my $level = shift;
530     my $dependency = shift;
531     our %dependencies;
532
533     if ($dependency =~ /config line\s+(.*)/) {
534
535        $dependencies{$level}{'config line'} = $1;
536     }
537 }
538
539 # XXX: somewhat misleading name
540 sub execute_regression_test ($) {
541
542     my $test_ref = shift;
543     my %test = %{$test_ref};
544     my $result = 0;
545
546     if ($test{'type'} == CLIENT_HEADER_TEST) {
547
548         $result = execute_client_header_regression_test($test_ref);
549
550     } elsif ($test{'type'} == SERVER_HEADER_TEST) {
551
552         $result = execute_server_header_regression_test($test_ref);
553
554     } elsif ($test{'type'} == DUMB_FETCH_TEST
555           or $test{'type'} == TRUSTED_CGI_REQUEST) {
556
557         $result = execute_dumb_fetch_test($test_ref);
558
559     } elsif ($test{'type'} == METHOD_TEST) {
560
561         $result = execute_method_test($test_ref);
562
563     } else {
564
565         die "Unsuported test type detected: " . $test{'type'};
566
567     }
568
569
570     return $result;
571 }
572
573 sub execute_method_test ($) {
574
575     my $test_ref = shift;
576     my %test = %{$test_ref};
577     my $buffer_ref;
578     my $result = 0;
579     my $status_code;
580     my $method = $test{'data'};
581
582     my $curl_parameters = '';
583     my $expected_status_code = $test{'expected-status-code'};
584
585     if ($method =~ /HEAD/i) {
586
587         $curl_parameters .= '--head ';
588
589     } else {
590
591         $curl_parameters .= '-X ' . $method . ' ';
592     }
593
594     $curl_parameters .= PRIVOXY_CGI_URL;
595
596     $buffer_ref = get_page_with_curl($curl_parameters);
597     $status_code = get_status_code($buffer_ref);
598
599     $result = check_status_code_result($status_code, $expected_status_code);
600
601     return $result;
602 }
603
604
605 sub execute_dumb_fetch_test ($) {
606
607     my $test_ref = shift;
608     my %test = %{$test_ref};
609     my $buffer_ref;
610     my $result = 0;
611     my $status_code;
612
613     my $curl_parameters = '';
614     my $expected_status_code = $test{'expected-status-code'};
615
616     if (defined $test{method}) {
617         $curl_parameters .= '-X ' . $test{method} . ' ';
618     }
619     if ($test{type} == TRUSTED_CGI_REQUEST) {
620         $curl_parameters .= '--referer ' . PRIVOXY_CGI_URL . ' ';
621     }
622
623     $curl_parameters .= $test{'data'};
624
625     $buffer_ref = get_page_with_curl($curl_parameters);
626     $status_code = get_status_code($buffer_ref);
627
628     $result = check_status_code_result($status_code, $expected_status_code);
629
630     return $result;
631 }
632
633 sub check_status_code_result ($$) {
634
635     my $status_code = shift;
636     my $expected_status_code = shift;
637     my $result = 0;
638
639     if ($expected_status_code == $status_code) {
640
641         $result = 1;
642         l(LL_VERBOSE_SUCCESS,
643           "Yay. We expected status code " . $expected_status_code . ", and received: " . $status_code . '.');
644
645     } elsif (cli_option_is_set('fuzzer-feeding') and $status_code == 123) {
646
647         l(LL_VERBOSE_FAILURE,
648           "Oh well. Status code lost while fuzzing. Can't check if it was " . $expected_status_code . '.');
649
650     } else {
651
652         l(LL_VERBOSE_FAILURE,
653           "Ooops. We expected status code " . $expected_status_code . ", but received: " . $status_code . '.');
654
655     }
656     
657     return $result;
658 }
659
660 sub execute_client_header_regression_test ($) {
661
662     my $test_ref = shift;
663     my $buffer_ref;
664     my $header;
665     my $result = 0;
666
667     $buffer_ref = get_show_request_with_curl($test_ref);
668
669     $header = get_header($buffer_ref, $test_ref);
670     $result = check_header_result($test_ref, $header);
671
672     return $result;
673 }
674
675 sub execute_server_header_regression_test ($) {
676
677     my $test_ref = shift;
678     my $buffer_ref;
679     my $header;
680     my $result = 0;
681
682     $buffer_ref = get_head_with_curl($test_ref);
683
684     $header = get_server_header($buffer_ref, $test_ref);
685     $result = check_header_result($test_ref, $header);
686
687     return $result;
688 }
689
690
691 sub interpret_result ($) {
692     my $success = shift;
693     return $success ? "Success" : "Failure";
694 }
695
696 sub check_header_result ($$) {
697
698     my $test_ref = shift;
699     my $header = shift;
700
701     my %test = %{$test_ref};
702     my $expect_header = $test{'expect-header'};
703     my $success = 0;
704
705     $header =~ s@   @ @g if defined($header);
706
707     if ($expect_header eq 'NO CHANGE') {
708
709         if (defined($header) and $header eq $test{'data'}) {
710
711             $success = 1;
712
713         } else {
714
715             $header //= "REMOVAL";
716             l(LL_VERBOSE_FAILURE,
717               "Ooops. Got: " . $header . " while expecting: " . $expect_header);
718         }
719
720     } elsif ($expect_header eq 'REMOVAL') {
721
722         if (defined($header) and $header eq $test{'data'}) {
723
724             l(LL_VERBOSE_FAILURE,
725               "Ooops. Expected removal but: " . $header . " is still there.");
726
727         } else {
728
729             # XXX: Use more reliable check here and make sure
730             # the header has a different name.
731             $success = 1;
732
733         }
734
735     } elsif ($expect_header eq 'SOME CHANGE') {
736
737         if (defined($header) and not $header eq $test{'data'}) {
738
739             $success = 1;
740
741         } else {
742
743             $header //= "REMOVAL";
744             l(LL_VERBOSE_FAILURE,
745               "Ooops. Got: " . $header . " while expecting: SOME CHANGE");
746         }
747
748
749     } else {
750
751         if (defined($header) and $header eq $expect_header) {
752
753             $success = 1;
754
755         } else {
756
757             $header //= "'No matching header'"; # XXX: No header detected to be precise
758             l(LL_VERBOSE_FAILURE,
759               "Ooops. Got: " . $header . " while expecting: " . $expect_header);
760         }
761     }
762     return $success;
763 }
764
765
766 sub get_header_name ($) {
767
768     my $header = shift;
769
770     $header =~ s@(.*?: ).*@$1@;
771
772     return $header;
773 }
774
775 sub get_header ($$) {
776
777     our $filtered_request = '';
778
779     my $buffer_ref = shift;
780     my $test_ref = shift;
781
782     my %test = %{$test_ref};
783     my @buffer = @{$buffer_ref};
784
785     my $expect_header = $test{'expect-header'};
786
787     my $line;
788     my $processed_request_reached = 0;
789     my $read_header = 0;
790     my $processed_request = '';
791     my $header;
792     my $header_to_get;
793
794     if ($expect_header eq 'REMOVAL'
795      or $expect_header eq 'NO CHANGE'
796      or  $expect_header eq 'SOME CHANGE') {
797
798         $expect_header = $test{'data'};
799
800     }
801
802     $header_to_get = get_header_name($expect_header);
803
804     foreach (@buffer) {
805
806         # Skip everything before the Processed request
807         if (/Processed Request/) {
808             $processed_request_reached = 1;
809             next;
810         }
811         next unless $processed_request_reached;
812
813         # End loop after the Processed request
814         last if (/<\/pre>/);
815
816         # Ditch tags and leading/trailing white space.
817         s@^\s*<.*?>@@g;
818         s@\s*$@@g;
819
820         $filtered_request .=  "\n" . $_;
821          
822         if (/^$header_to_get/) {
823             $read_header = 1;
824             $header = $_;
825             last;
826         }
827     }
828
829     return $header;
830 }
831
832 sub get_server_header ($$) {
833
834     my $buffer_ref = shift;
835     my $test_ref = shift;
836
837     my %test = %{$test_ref};
838     my @buffer = @{$buffer_ref};
839
840     my $expect_header = $test{'expect-header'};
841     my $header;
842     my $header_to_get;
843
844     if ($expect_header eq 'REMOVAL'
845      or $expect_header eq 'NO CHANGE'
846      or $expect_header eq 'SOME CHANGE') {
847
848         $expect_header = $test{'data'};
849
850     }
851
852     $header_to_get = get_header_name($expect_header);
853
854     foreach (@buffer) {
855
856         # XXX: shoul probably verify that the request
857         # was actually answered by Fellatio.
858         if (/^$header_to_get/) {
859             $header = $_;
860             $header =~ s@\s*$@@g;
861             last;
862         }
863     }
864
865     return $header;
866 }
867
868 sub get_header_to_check ($) {
869
870     # No longer in use but not removed yet.
871
872     my $buffer_ref = shift;
873     my $header;
874     my @buffer = @{$buffer_ref}; 
875     my $line;
876     my $processed_request_reached = 0;
877     my $read_header = 0;
878     my $processed_request = '';
879
880     l(LL_ERROR, "You are not supposed to use get_header_to_()!");
881
882     foreach (@buffer) {
883
884         # Skip everything before the Processed request
885         if (/Processed Request/) {
886             $processed_request_reached = 1;
887             next;
888         }
889         next unless $processed_request_reached;
890
891         # End loop after the Processed request
892         last if (/<\/pre>/);
893
894         # Ditch tags and leading/trailing white space.
895         s@^\s*<.*?>@@g;
896         s@\s*$@@g;
897
898         $processed_request .= $_;
899          
900         if (/^X-Privoxy-Regression-Test/) {
901             $read_header = 1;
902             next;
903         }
904
905         if ($read_header) {
906             $header = $_;
907             $read_header = 0;
908         }
909
910     }
911
912     return $header;
913 }
914
915 sub get_status_code ($) {
916
917     my $buffer_ref = shift;
918     my @buffer = @{$buffer_ref}; 
919
920     foreach (@buffer) {
921
922         if (/^HTTP\/\d\.\d (\d{3})/) {
923
924             return $1;
925
926         } else {
927
928             return '123' if cli_option_is_set('fuzzer-feeding');
929             chomp;
930             l(LL_ERROR, 'Unexpected buffer line: "' . $_ . '"');
931         }
932     }
933 }
934
935 sub get_test_keys () {
936     return ('tag', 'data', 'expect-header', 'ignore');
937 }
938
939 # XXX: incomplete
940 sub test_content_as_string ($) {
941
942     my $test_ref = shift;
943     my %test = %{$test_ref};
944
945     my $s = "\n\t";
946
947     foreach my $key (get_test_keys()) {
948         $test{$key} = 'Not set' unless (defined $test{$key});
949     }
950
951     $s .= 'Tag: ' . $test{'tag'};
952     $s .= "\n\t";
953     $s .= 'Set header: ' . $test{'data'}; # XXX: adjust for other test types
954     $s .= "\n\t";
955     $s .= 'Expected header: ' . $test{'expect-header'};
956     $s .= "\n\t";
957     $s .= 'Ignore: ' . $test{'ignore'};
958
959     return $s;
960 }
961
962 ############################################################################
963 #
964 # HTTP fetch functions
965 #
966 ############################################################################
967
968 sub check_for_curl () {
969     my $curl = CURL;
970     l(LL_ERROR, "No curl found.") unless (`which $curl`);
971 }
972
973 sub get_cgi_page_or_else ($) {
974
975     my $cgi_url = shift;
976     my $content_ref = get_page_with_curl($cgi_url);
977     my $status_code = get_status_code($content_ref);
978
979     if (200 != $status_code) {
980
981         my $log_message = "Failed to fetch Privoxy CGI Page. " .
982                           "Received status code ". $status_code .
983                           " while only 200 is acceptable.";
984
985         if (cli_option_is_set('fuzzer-feeding')) {
986
987             $log_message .= " Ignored due to fuzzer feeding.";
988             l(LL_SOFT_ERROR, $log_message)
989
990         } else {
991
992             l(LL_ERROR, $log_message);
993
994         }
995     }
996     
997     return $content_ref;
998 }
999
1000 sub get_show_request_with_curl ($) {
1001
1002     our $privoxy_cgi_url;
1003     my $test_ref = shift;
1004     my %test = %{$test_ref};
1005
1006     my $curl_parameters = ' ';
1007
1008     # Enable the action to test
1009     $curl_parameters .= '-H \'X-Privoxy-Control: ' . $test{'tag'} . '\' ';
1010     # The header to filter
1011     $curl_parameters .= '-H \'' . $test{'data'} . '\' ';
1012
1013     $curl_parameters .= ' ';
1014     $curl_parameters .= $privoxy_cgi_url;
1015     $curl_parameters .= 'show-request';
1016
1017     return get_cgi_page_or_else($curl_parameters);
1018 }
1019
1020
1021 sub get_head_with_curl ($) {
1022
1023     our $fellatio_url = FELLATIO_URL;
1024     my $test_ref = shift;
1025     my %test = %{$test_ref};
1026
1027     my $curl_parameters = ' ';
1028
1029     # Enable the action to test
1030     $curl_parameters .= '-H \'X-Privoxy-Control: ' . $test{'tag'} . '\' ';
1031     # The header to filter
1032     $curl_parameters .= '-H \'X-Gimme-Head-With: ' . $test{'data'} . '\' ';
1033     $curl_parameters .= '--head ';
1034
1035     $curl_parameters .= ' ';
1036     $curl_parameters .= $fellatio_url;
1037
1038     return get_page_with_curl($curl_parameters);
1039 }
1040
1041
1042 sub get_page_with_curl ($) {
1043
1044     my $parameters = shift;
1045     my @buffer;
1046     my $curl_line = CURL;
1047     my $retries_left = get_cli_option('retries') + 1;
1048     my $failure_reason;
1049
1050     if (cli_option_is_set('privoxy-address')) {
1051         $curl_line .= ' --proxy ' . get_cli_option('privoxy-address');
1052     }
1053
1054     # Let Privoxy emit two log messages less.
1055     $curl_line .= ' -H \'Proxy-Connection:\' ' unless $parameters =~ /Proxy-Connection:/;
1056     $curl_line .= ' -H \'Connection: close\' ' unless $parameters =~ /Connection:/;
1057     # We don't care about fetch statistic.
1058     $curl_line .= " -s ";
1059     # We do care about the failure reason if any.
1060     $curl_line .= " -S ";
1061     # We want to see the HTTP status code
1062     $curl_line .= " --include ";
1063     # We want to advertise ourselves
1064     $curl_line .= " --user-agent '" . PRT_VERSION . "' ";
1065     # We aren't too patient
1066     $curl_line .= " --max-time '" . get_cli_option('max-time') . "' ";
1067
1068     $curl_line .= $parameters;
1069     # XXX: still necessary?
1070     $curl_line .= ' 2>&1';
1071
1072     l(LL_PAGE_FETCHING, "Executing: " . $curl_line);
1073
1074     do {
1075         @buffer = `$curl_line`;
1076
1077         if ($?) {
1078             $failure_reason = array_as_string(\@buffer);
1079             chomp $failure_reason;
1080             l(LL_SOFT_ERROR, "Fetch failure: '" . $failure_reason . $! ."'");
1081         }
1082     } while ($? && --$retries_left);
1083
1084     unless ($retries_left) {
1085         l(LL_ERROR,
1086           "Running curl failed " . get_cli_option('retries') .
1087           " times in a row. Last error: '" . $failure_reason . "'.");
1088     }
1089
1090     return \@buffer;
1091 }
1092
1093
1094 ############################################################################
1095 #
1096 # Log functions
1097 #
1098 ############################################################################
1099
1100 sub array_as_string ($) {
1101     my $array_ref = shift;
1102     my $string = '';
1103
1104     foreach (@{$array_ref}) {
1105         $string .= $_;
1106     }
1107
1108     return $string;
1109 }
1110
1111
1112 sub show_test ($) {
1113     my $test_ref = shift;
1114     log_message('Test is:' . test_content_as_string($test_ref));
1115 }
1116
1117 # Conditional log
1118 sub l ($$) {
1119     our $log_level;
1120     my $this_level = shift;
1121     my $message = shift;
1122
1123     return unless ($log_level & $this_level);
1124
1125     if (LL_ERROR & $this_level) {
1126         $message = 'Oh noes. ' . $message . ' Fatal error. Exiting.';
1127     }
1128
1129     log_message($message);
1130
1131     if (LL_ERROR & $this_level) {
1132         exit;
1133     }
1134 }
1135
1136 sub log_message ($) {
1137
1138     my $message = shift;
1139
1140     our $logfile;
1141     our $no_logging;
1142     our $leading_log_date;
1143     our $leading_log_time;
1144
1145     my $time_stamp = '';
1146     my ( $sec, $min, $hour, $mday, $mon, $year, $wday, $yday, $isdst ) = localtime time;
1147
1148     if ($leading_log_date || $leading_log_time) {
1149
1150         if ($leading_log_date) {
1151             $year += 1900;
1152             $mon  += 1;
1153             $time_stamp = sprintf("%i/%.2i/%.2i", $year, $mon, $mday);
1154         }
1155
1156         if ($leading_log_time) {
1157             $time_stamp .= ' ' if $leading_log_date;
1158             $time_stamp.= sprintf("%.2i:%.2i:%.2i", $hour, $min, $sec);
1159         }
1160         
1161         $message = $time_stamp . ": " . $message;
1162     }
1163
1164
1165     printf(STDERR "%s\n", $message);
1166
1167 }
1168
1169 sub log_result ($$) {
1170
1171     our $verbose_test_description;
1172     our $filtered_request;
1173
1174     my $test_ref = shift;
1175     my $result = shift;
1176     my $number = shift;
1177
1178     my %test = %{$test_ref};
1179     my $message = '';
1180
1181     $message .= interpret_result($result);
1182     $message .= " for test ";
1183     $message .= $number;
1184     $message .= '/';
1185     $message .= $test{'number'};
1186     $message .= '/';
1187     $message .= $test{'section-id'};
1188     $message .= '/';
1189     $message .= $test{'regression-test-id'};
1190     $message .= '.';
1191
1192     if ($verbose_test_description) {
1193
1194         if ($test{'type'} == CLIENT_HEADER_TEST) {
1195
1196             $message .= ' Header ';
1197             $message .= quote($test{'data'});
1198             $message .= ' and tag ';
1199             $message .= quote($test{'tag'});
1200
1201         } elsif ($test{'type'} == SERVER_HEADER_TEST) {
1202
1203             $message .= ' Request Header ';
1204             $message .= quote($test{'data'});
1205             $message .= ' and tag ';
1206             $message .= quote($test{'tag'});
1207
1208         } elsif ($test{'type'} == DUMB_FETCH_TEST) {
1209
1210             $message .= ' URL ';
1211             $message .= quote($test{'data'});
1212             $message .= ' and expected status code ';
1213             $message .= quote($test{'expected-status-code'});
1214
1215         } elsif ($test{'type'} == TRUSTED_CGI_REQUEST) {
1216
1217             $message .= ' CGI URL ';
1218             $message .= quote($test{'data'});
1219             $message .= ' and expected status code ';
1220             $message .= quote($test{'expected-status-code'});
1221
1222         } elsif ($test{'type'} == METHOD_TEST) {
1223
1224             $message .= ' HTTP method ';
1225             $message .= quote($test{'data'});
1226             $message .= ' and expected status code ';
1227             $message .= quote($test{'expected-status-code'});
1228
1229         } else {
1230
1231             die "Incomplete support for test type " . $test{'type'} .  " detected.";
1232
1233         }
1234     }
1235
1236     log_message($message) unless ($result && cli_option_is_set('silent'));
1237 }
1238
1239 sub quote ($) {
1240     my $s = shift;
1241     return '\'' . $s . '\'';
1242 }
1243
1244 sub print_version () {
1245     printf PRT_VERSION . "\n" . 'Copyright (C) 2007-2008 Fabian Keil <fk@fabiankeil.de>' . "\n";
1246 }
1247
1248 sub help () {
1249
1250     our %cli_options;
1251
1252     print_version();
1253
1254     print << "    EOF"
1255
1256 Options and their default values if they have any:
1257     [--debug $cli_options{'debug'}]
1258     [--fuzzer-feeding]
1259     [--help]
1260     [--level]
1261     [--loops $cli_options{'loops'}]
1262     [--max-level $cli_options{'max-level'}]
1263     [--max-time $cli_options{'max-time'}]
1264     [--min-level $cli_options{'min-level'}]
1265     [--privoxy-address]
1266     [--retries $cli_options{'retries'}]
1267     [--silent]
1268     [--version]
1269 see "perldoc $0" for more information
1270     EOF
1271     ;
1272     exit(0);
1273 }
1274
1275 sub init_cli_options () {
1276
1277     our %cli_options;
1278     our $log_level;
1279
1280     $cli_options{'min-level'} = CLI_MIN_LEVEL;
1281     $cli_options{'max-level'} = CLI_MAX_LEVEL;
1282     $cli_options{'debug'}  = $log_level;
1283     $cli_options{'loops'}  = CLI_LOOPS;
1284     $cli_options{'max-time'}  = CLI_MAX_TIME;
1285     $cli_options{'retries'}  = CLI_RETRIES;
1286 }
1287
1288 sub parse_cli_options () {
1289
1290     our %cli_options;
1291     our $log_level;
1292
1293     init_cli_options();
1294
1295     GetOptions (
1296                 'debug=s' => \$cli_options{'debug'},
1297                 'help'     => sub { help },
1298                 'silent' => \$cli_options{'silent'},
1299                 'min-level=s' => \$cli_options{'min-level'},
1300                 'max-level=s' => \$cli_options{'max-level'},
1301                 'privoxy-address=s' => \$cli_options{'privoxy-address'},
1302                 'level=s' => \$cli_options{'level'},
1303                 'loops=s' => \$cli_options{'loops'},
1304                 'test-number=s' => \$cli_options{'test-number'},
1305                 'fuzzer-feeding' => \$cli_options{'fuzzer-feeding'},
1306                 'retries=s' => \$cli_options{'retries'},
1307                 'max-time=s' => \$cli_options{'max-time'},
1308                 'version'  => sub { print_version && exit(0) }
1309     );
1310     $log_level |= $cli_options{'debug'};
1311 }
1312
1313 sub cli_option_is_set ($) {
1314
1315     our %cli_options;
1316     my $cli_option = shift;
1317
1318     return defined $cli_options{$cli_option};
1319 }
1320
1321 sub get_cli_option ($) {
1322
1323     our %cli_options;
1324     my $cli_option = shift;
1325
1326     die "Unknown CLI option: $cli_option" unless defined $cli_options{$cli_option};
1327
1328     return $cli_options{$cli_option};
1329 }
1330
1331 sub main () {
1332
1333     init_our_variables();
1334     parse_cli_options();
1335     check_for_curl();
1336     load_regressions_tests();
1337     execute_regression_tests();
1338 }
1339
1340 main();
1341
1342 =head1 NAME
1343
1344 B<privoxy-regression-test> - A regression test "framework" for Privoxy.
1345
1346 =head1 SYNOPSIS
1347
1348 B<privoxy-regression-test> [B<--debug bitmask>] [B<--fuzzer-feeding>] [B<--help>]
1349 [B<--level level>] [B<--loops count>] [B<--max-level max-level>]
1350 [B<--max-time max-time>] [B<--min-level min-level>] B<--privoxy-address proxy-address>
1351 [B<--retries retries>] [B<--silent>] [B<--version>]
1352
1353 =head1 DESCRIPTION
1354
1355 Privoxy-Regression-Test is supposed to one day become
1356 a regression test suite for Privoxy. It's not quite there
1357 yet, however, and can currently only test client header
1358 actions and check the returned status code for requests
1359 to arbitrary URLs.
1360
1361 Client header actions are tested by requesting
1362 B<http://config.privoxy.org/show-request> and checking whether
1363 or not Privoxy modified the original request as expected.
1364
1365 The original request contains both the header the action-to-be-tested
1366 acts upon and an additional tagger-triggering header that enables
1367 the action to test.
1368
1369 =head1 CONFIGURATION FILE SYNTAX
1370
1371 Privoxy-Regression-Test's configuration is embedded in
1372 Privoxy action files and loaded through Privoxy's web interface.
1373
1374 It makes testing a Privoxy version running on a remote system easier
1375 and should prevent you from updating your tests without updating Privoxy's
1376 configuration accordingly.
1377
1378 A client-header-action test section looks like this:
1379
1380     # Set Header    = Referer: http://www.example.org.zwiebelsuppe.exit/
1381     # Expect Header = Referer: http://www.example.org/
1382     {+client-header-filter{hide-tor-exit-notation} -hide-referer}
1383     TAG:^client-header-filter\{hide-tor-exit-notation\}$
1384
1385 The example above causes Privoxy-Regression-Test to set
1386 the header B<Referer: http://www.example.org.zwiebelsuppe.exit/>
1387 and to expect it to be modified to
1388 B<Referer: http://www.example.org/>.
1389
1390 When testing this section, Privoxy-Regression-Test will set the header
1391 B<X-Privoxy-Control: client-header-filter{hide-tor-exit-notation}>
1392 causing the B<privoxy-control> tagger to create the tag
1393 B<client-header-filter{hide-tor-exit-notation}> which will finally
1394 cause Privoxy to enable the action section.
1395
1396 Note that the actions itself are only used by Privoxy,
1397 Privoxy-Regression-Test ignores them and will be happy
1398 as long as the expectations are satisfied.
1399
1400 A fetch test looks like this:
1401
1402     # Fetch Test = http://p.p/user-manual
1403     # Expect Status Code = 302
1404
1405 It tells Privoxy-Regression-Test to request B<http://p.p/user-manual>
1406 and to expect a response with the HTTP status code B<302>. Obviously that's
1407 not a very thorough test and mainly useful to get some code coverage
1408 for Valgrind or to verify that the templates are installed correctly.
1409
1410 If you want to test CGI pages that require a trusted
1411 referer, you can use:
1412
1413     # Trusted CGI Request =  http://p.p/edit-actions
1414
1415 It works like ordinary fetch tests, but sets the referer
1416 header to a trusted value.
1417
1418 If no explicit status code expectation is set, B<200> is used.
1419
1420 Additionally all tests have test levels to let the user
1421 control which ones to execute (see I<OPTIONS> below). 
1422 Test levels are either set with the B<Level> directive,
1423 or implicitly through the test type.
1424
1425 Fetch tests default to level 6, tests for trusted
1426 CGI requests to level 3 and client-header-action tests
1427 to level 1.
1428
1429 =head1 OPTIONS
1430
1431 B<--debug bitmask> Add the bitmask provided as integer
1432 to the debug settings.
1433
1434 B<--fuzzer-feeding> Ignore some errors that would otherwise
1435 cause Privoxy-Regression-Test to abort the test because
1436 they shouldn't happen in normal operation. This option is
1437 intended to be used if Privoxy-Regression-Test is only
1438 used to feed a fuzzer in which case there's a high chance
1439 that Privoxy gets an invalid request and returns an error
1440 message.
1441
1442 B<--help> Shows available command line options.
1443
1444 B<--level level> Only execute tests with the specified B<level>. 
1445
1446 B<--loop count> Loop through the regression tests B<count> times. 
1447 Useful to feed a fuzzer, or when doing stress tests with
1448 several Privoxy-Regression-Test instances running at the same
1449 time.
1450
1451 B<--max-level max-level> Only execute tests with a B<level>
1452 below or equal to the numerical B<max-level>.
1453
1454 B<--max-time max-time> Give Privoxy B<max-time> seconds
1455 to return data. Increasing the default may make sense when
1456 Privoxy is run through Valgrind, decreasing the default may
1457 make sense when Privoxy-Regression-Test is used to feed
1458 a fuzzer.
1459
1460 B<--min-level min-level> Only execute tests with a B<level>
1461 above or equal to the numerical B<min-level>.
1462
1463 B<--privoxy-address proxy-address> Privoxy's listening address.
1464 If it's not set, the value of the environment variable http_proxy
1465 will be used. B<proxy-address> has to be specified in http_proxy
1466 syntax.
1467
1468 B<--retries retries> Retry B<retries> times.
1469
1470 B<--silent> Don't log succesful test runs.
1471
1472 B<--version> Print version and exit.
1473
1474 The second dash is optional, options can be shortened,
1475 as long as there are no ambiguities.
1476
1477 =head1 PRIVOXY CONFIGURATION
1478
1479 Privoxy-Regression-Test is shipped with B<regression-tests.action>
1480 which aims to test all official client-header modifying actions
1481 and can be used to verify that the templates and the user manual
1482 files are installed correctly.
1483
1484 To use it, it has to be copied in Privoxy's configuration
1485 directory, and afterwards referenced in Privoxy's configuration
1486 file with the line:
1487
1488     actionsfile regression-tests.action
1489
1490 In general, its tests are supposed to work without changing
1491 any other action files, unless you already added lots of
1492 taggers yourself. If you are using taggers that cause problems,
1493 you might have to temporary disable them for Privoxy's CGI pages.
1494
1495 Some of the regression tests rely on Privoxy features that
1496 may be disabled in your configuration. Tests with a level below
1497 7 are supposed to work with all Privoxy configurations (provided
1498 you didn't build with FEATURE_GRACEFUL_TERMINATION).
1499
1500 Tests with level 9 require Privoxy to deliver the User Manual,
1501 tests with level 12 require the CGI editor to be enabled.
1502
1503 =head1 CAVEATS
1504
1505 Expect the configuration file syntax to change with future releases.
1506
1507 =head1 LIMITATIONS
1508
1509 As Privoxy's B<show-request> page only shows client headers,
1510 Privoxy-Regression-Test can't use it to test Privoxy actions
1511 that modify server headers.
1512
1513 As Privoxy-Regression-Test relies on Privoxy's tag feature to
1514 control the actions to test, it currently only works with
1515 Privoxy 3.0.7 or later.
1516
1517 At the moment Privoxy-Regression-Test fetches Privoxy's
1518 configuration page through I<curl>(1), therefore you have to
1519 have I<curl> installed, otherwise you won't be able to run
1520 Privoxy-Regression-Test in a meaningful way.
1521
1522 =head1 SEE ALSO
1523
1524 privoxy(1) curl(1)
1525
1526 =head1 AUTHOR
1527
1528 Fabian Keil <fk@fabiankeil.de>
1529
1530 =cut