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