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