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