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