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