Rearrange struct client_state to reduce memory on amd64
[privoxy.git] / tools / privoxy-regression-test.pl
index ee0bd63..5c1a86d 100755 (executable)
@@ -7,8 +7,6 @@
 # A regression test "framework" for Privoxy. For documentation see:
 # perldoc privoxy-regression-test.pl
 #
 # A regression test "framework" for Privoxy. For documentation see:
 # perldoc privoxy-regression-test.pl
 #
-# $Id: privoxy-regression-test.pl,v 1.188 2009/06/15 18:13:34 fk Exp $
-#
 # Wish list:
 #
 # - Update documentation
 # Wish list:
 #
 # - Update documentation
@@ -19,7 +17,7 @@
 # - Document magic Expect Header values
 # - Internal fuzz support?
 #
 # - Document magic Expect Header values
 # - Internal fuzz support?
 #
-# Copyright (c) 2007-2009 Fabian Keil <fk@fabiankeil.de>
+# Copyright (c) 2007-2020 Fabian Keil <fk@fabiankeil.de>
 #
 # Permission to use, copy, modify, and distribute this software for any
 # purpose with or without fee is hereby granted, provided that the above
 #
 # Permission to use, copy, modify, and distribute this software for any
 # purpose with or without fee is hereby granted, provided that the above
@@ -40,7 +38,7 @@ use strict;
 use Getopt::Long;
 
 use constant {
 use Getopt::Long;
 
 use constant {
-    PRT_VERSION => 'Privoxy-Regression-Test 0.3',
+    PRT_VERSION => 'Privoxy-Regression-Test 0.7.2',
  
     CURL => 'curl',
 
  
     CURL => 'curl',
 
@@ -49,10 +47,13 @@ use constant {
     CLI_LOOPS     => 1,
     CLI_MAX_TIME  => 5,
     CLI_MIN_LEVEL => 0,
     CLI_LOOPS     => 1,
     CLI_MAX_TIME  => 5,
     CLI_MIN_LEVEL => 0,
-    # XXX: why limit at all?
+    # The reason for a maximum test level is explained in the
+    # perldoc section TEST LEVELS near the end of this file.
     CLI_MAX_LEVEL => 100,
     CLI_FORKS     => 0,
     CLI_MAX_LEVEL => 100,
     CLI_FORKS     => 0,
+    CLI_SLEEP_TIME => 0,
 
 
+    PRIVOXY_ADDRESS  => 'http://127.0.0.1:8118/',
     PRIVOXY_CGI_URL  => 'http://p.p/',
     FELLATIO_URL     => 'http://127.0.0.1:8080/',
     LEADING_LOG_DATE => 1,
     PRIVOXY_CGI_URL  => 'http://p.p/',
     FELLATIO_URL     => 'http://127.0.0.1:8080/',
     LEADING_LOG_DATE => 1,
@@ -65,8 +66,6 @@ use constant {
     DEBUG_LEVEL_VERBOSE_SUCCESS => 0,
     DEBUG_LEVEL_STATUS          => 1,
 
     DEBUG_LEVEL_VERBOSE_SUCCESS => 0,
     DEBUG_LEVEL_STATUS          => 1,
 
-    VERBOSE_TEST_DESCRIPTION    => 1,
-
     # Internal use, don't modify
     # Available debug bits:
     LL_SOFT_ERROR       =>  1,
     # Internal use, don't modify
     # Available debug bits:
     LL_SOFT_ERROR       =>  1,
@@ -83,22 +82,19 @@ use constant {
     STICKY_ACTIONS_TEST =>  5,
     TRUSTED_CGI_REQUEST =>  6,
     BLOCK_TEST          =>  7,
     STICKY_ACTIONS_TEST =>  5,
     TRUSTED_CGI_REQUEST =>  6,
     BLOCK_TEST          =>  7,
+    REDIRECT_TEST       =>108,
 };
 
 };
 
-sub init_our_variables () {
+sub init_our_variables() {
 
     our $leading_log_time = LEADING_LOG_TIME;
     our $leading_log_date = LEADING_LOG_DATE;
 
     our $leading_log_time = LEADING_LOG_TIME;
     our $leading_log_date = LEADING_LOG_DATE;
-
     our $privoxy_cgi_url  = PRIVOXY_CGI_URL;
     our $privoxy_cgi_url  = PRIVOXY_CGI_URL;
-
-    our $verbose_test_description = VERBOSE_TEST_DESCRIPTION;
-
     our $log_level = get_default_log_level();
     our $log_level = get_default_log_level();
-
+    our $proxy = defined $ENV{'http_proxy'} ? $ENV{'http_proxy'} : PRIVOXY_ADDRESS;
 }
 
 }
 
-sub get_default_log_level () {
+sub get_default_log_level() {
     
     my $log_level = 0;
 
     
     my $log_level = 0;
 
@@ -120,7 +116,7 @@ sub get_default_log_level () {
 #
 ############################################################################
 
 #
 ############################################################################
 
-sub parse_tag ($) {
+sub parse_tag($) {
 
     my $tag = shift;
 
 
     my $tag = shift;
 
@@ -136,10 +132,10 @@ sub parse_tag ($) {
     return $tag;
 }
 
     return $tag;
 }
 
-sub check_for_forbidden_characters ($) {
+sub check_for_forbidden_characters($) {
 
     my $string = shift;
 
     my $string = shift;
-    my $allowed = '[-=\dA-Za-z~{}:./();\t ,+@"_%?&*^]';
+    my $allowed = '[-=\dA-Za-z~{}\[\]:./();\t ,+@"_%?&*^|]';
 
     unless ($string =~ m/^$allowed*$/o) {
         my $forbidden = $string;
 
     unless ($string =~ m/^$allowed*$/o) {
         my $forbidden = $string;
@@ -149,7 +145,162 @@ sub check_for_forbidden_characters ($) {
     }
 }
 
     }
 }
 
-sub load_regressions_tests () {
+sub load_regression_tests() {
+    if (cli_option_is_set('local-test-file')) {
+        load_regression_tests_from_file(get_cli_option('local-test-file'));
+    } else {
+        load_regression_tests_through_privoxy();
+    }
+}
+
+# XXX: Contains a lot of code duplicated from load_action_files()
+#      that should be factored out.
+sub load_regression_tests_from_file($) {
+    my $action_file = shift;
+
+    # initialized here
+    our %actions;
+    our @regression_tests;
+
+    my $si = 0;  # Section index
+    my $ri = -1; # Regression test index
+    my $count = 0;
+
+    my $ignored = 0;
+
+    my $sticky_actions = undef;
+
+    l(LL_STATUS, "Gathering regression tests from local file " . $action_file);
+
+    open(my $ACTION_FILE, "<", $action_file)
+        or log_and_die("Failed to open $action_file: $!");
+
+    while (<$ACTION_FILE>) {
+
+        my $no_checks = 0;
+        chomp;
+        my ($token, $value) = tokenize($_);
+
+        next unless defined $token;
+
+        # Load regression tests
+
+        if (token_starts_new_test($token)) {
+
+            # Beginning of new regression test.
+            $ri++;
+            $count++;
+            enlist_new_test(\@regression_tests, $token, $value, $si, $ri, $count);
+            $no_checks = 1; # Already validated by enlist_new_test().
+        }
+
+        if ($token =~ /level\s+(\d+)/i) {
+
+            my $level = $1;
+            register_dependency($level, $value);
+        }
+
+        if ($token eq 'sticky actions') {
+
+            # Will be used by each following Sticky URL.
+            $sticky_actions = $value;
+            if ($sticky_actions =~ /{[^}]*\s/) {
+                log_and_die("'Sticky Actions' with whitespace inside the " .
+                            "action parameters are currently unsupported.");
+            }
+        }
+
+        if ($si == -1 || $ri == -1) {
+            # No beginning of a test detected yet,
+            # so we don't care about any other test
+            # attributes.
+            next;
+        }
+
+        if ($token eq 'expect header') {
+
+            l(LL_FILE_LOADING, "Detected expectation: " . $value);
+            $regression_tests[$si][$ri]{'expect-header'} = $value;
+
+        } elsif ($token eq 'tag') {
+
+            next if ($ri == -1);
+
+            my $tag = parse_tag($value);
+
+            # We already checked in parse_tag() after filtering
+            $no_checks = 1;
+
+            l(LL_FILE_LOADING, "Detected TAG: " . $tag);
+
+            # Save tag for all tests in this section
+            do {
+                $regression_tests[$si][$ri]{'tag'} = $tag;
+            } while ($ri-- > 0);
+
+            $si++;
+            $ri = -1;
+
+        } elsif ($token eq 'ignore' && $value =~ /Yes/i) {
+
+            l(LL_FILE_LOADING, "Ignoring section: " . test_content_as_string($regression_tests[$si][$ri]));
+            $regression_tests[$si][$ri]{'ignore'} = 1;
+            $ignored++;
+
+        } elsif ($token eq 'expect status code') {
+
+            l(LL_FILE_LOADING, "Expecting status code: " . $value);
+            $regression_tests[$si][$ri]{'expected-status-code'} = $value;
+
+        } elsif ($token eq 'level') { # XXX: stupid name
+
+            $value =~ s@(\d+).*@$1@;
+            l(LL_FILE_LOADING, "Level: " . $value);
+            $regression_tests[$si][$ri]{'level'} = $value;
+
+        } elsif ($token eq 'method') {
+
+            l(LL_FILE_LOADING, "Method: " . $value);
+            $regression_tests[$si][$ri]{'method'} = $value;
+
+        } elsif ($token eq 'redirect destination') {
+
+            l(LL_FILE_LOADING, "Redirect destination: " . $value);
+            $regression_tests[$si][$ri]{'redirect destination'} = $value;
+
+        } elsif ($token eq 'url') {
+
+            if (defined $sticky_actions) {
+                die "WTF? Attempted to overwrite Sticky Actions"
+                    if defined ($regression_tests[$si][$ri]{'sticky-actions'});
+
+                l(LL_FILE_LOADING, "Sticky actions: " . $sticky_actions);
+                $regression_tests[$si][$ri]{'sticky-actions'} = $sticky_actions;
+            } else {
+                log_and_die("Sticky URL without Sticky Actions in $action_file: $value");
+            }
+
+        } else {
+
+            # We don't use it, so we don't need
+            $no_checks = 1;
+            l(LL_STATUS, "Enabling no_checks for $token") unless $no_checks;
+        }
+
+        # XXX: Necessary?
+        unless ($no_checks)  {
+            check_for_forbidden_characters($value);
+            check_for_forbidden_characters($token);
+        }
+    }
+
+    l(LL_FILE_LOADING, "Done loading " . $count . " regression tests."
+      . " Of which " . $ignored. " will be ignored)\n");
+
+}
+
+
+sub load_regression_tests_through_privoxy() {
 
     our $privoxy_cgi_url;
     our @privoxy_config;
 
     our $privoxy_cgi_url;
     our @privoxy_config;
@@ -158,6 +309,7 @@ sub load_regressions_tests () {
     my $curl_url = '';
     my $file_number = 0;
     my $feature;
     my $curl_url = '';
     my $file_number = 0;
     my $feature;
+    my $privoxy_version = '(Unknown version!)';
 
     $curl_url .= $privoxy_cgi_url;
     $curl_url .= 'show-status';
 
     $curl_url .= $privoxy_cgi_url;
     $curl_url .= 'show-status';
@@ -189,20 +341,24 @@ sub load_regressions_tests () {
 
             $privoxy_features{$feature} = $1 if defined $feature;
             $feature = undef;
 
             $privoxy_features{$feature} = $1 if defined $feature;
             $feature = undef;
+
+        } elsif (m@This is <a href="https?://www.privoxy.org/">Privoxy</a> (\d+\.\d+\.\d+) on@) {
+            $privoxy_version = $1;
         }
     }
 
         }
     }
 
-    l(LL_FILE_LOADING, "Recognized " . @actionfiles . " actions files");
+    l(LL_STATUS, "Gathering regression tests from " .
+      @actionfiles . " action file(s) delivered by Privoxy $privoxy_version.");
 
     load_action_files(\@actionfiles);
 }
 
 
     load_action_files(\@actionfiles);
 }
 
-sub token_starts_new_test ($) {
+sub token_starts_new_test($) {
 
     my $token = shift;
     my @new_test_directives = ('set header', 'fetch test',
          'trusted cgi request', 'request header', 'method test',
 
     my $token = shift;
     my @new_test_directives = ('set header', 'fetch test',
          'trusted cgi request', 'request header', 'method test',
-         'blocked url', 'url');
+         'blocked url', 'url', 'redirected url');
 
     foreach my $new_test_directive (@new_test_directives) {
         return 1 if $new_test_directive eq $token;
 
     foreach my $new_test_directive (@new_test_directives) {
         return 1 if $new_test_directive eq $token;
@@ -211,16 +367,17 @@ sub token_starts_new_test ($) {
     return 0;
 }
 
     return 0;
 }
 
-sub tokenize ($) {
+sub tokenize($) {
 
     my ($token, $value) = (undef, undef);
 
 
     my ($token, $value) = (undef, undef);
 
-    # Remove leading and trailing white space.
-    s@^\s*@@;
+    # Remove leading and trailing white space and a
+    # a leading <pre> which is part of the first line.
+    s@^\s*(<pre>)?@@;
     s@\s*$@@;
 
     # Reverse HTML-encoding
     s@\s*$@@;
 
     # Reverse HTML-encoding
-    # XXX: Seriously imcomplete. 
+    # XXX: Seriously incomplete.
     s@&quot;@"@g;
     s@&amp;@&@g;
 
     s@&quot;@"@g;
     s@&amp;@&@g;
 
@@ -242,49 +399,63 @@ sub tokenize ($) {
     return ($token, $value);
 }
 
     return ($token, $value);
 }
 
-sub enlist_new_test ($$$$$$) {
+sub enlist_new_test($$$$$$) {
 
     my ($regression_tests, $token, $value, $si, $ri, $number) = @_;
     my $type;
 
     my ($regression_tests, $token, $value, $si, $ri, $number) = @_;
     my $type;
+    my $executor;
 
     if ($token eq 'set header') {
 
         l(LL_FILE_LOADING, "Header to set: " . $value);
         $type = CLIENT_HEADER_TEST;
 
     if ($token eq 'set header') {
 
         l(LL_FILE_LOADING, "Header to set: " . $value);
         $type = CLIENT_HEADER_TEST;
+        $executor = \&execute_client_header_regression_test;
 
     } elsif ($token eq 'request header') {
 
         l(LL_FILE_LOADING, "Header to request: " . $value);
         $type = SERVER_HEADER_TEST;
 
     } elsif ($token eq 'request header') {
 
         l(LL_FILE_LOADING, "Header to request: " . $value);
         $type = SERVER_HEADER_TEST;
+        $executor = \&execute_server_header_regression_test;
         $$regression_tests[$si][$ri]{'expected-status-code'} = 200;
 
     } elsif ($token eq 'trusted cgi request') {
 
         l(LL_FILE_LOADING, "CGI URL to test in a dumb way: " . $value);
         $type = TRUSTED_CGI_REQUEST;
         $$regression_tests[$si][$ri]{'expected-status-code'} = 200;
 
     } elsif ($token eq 'trusted cgi request') {
 
         l(LL_FILE_LOADING, "CGI URL to test in a dumb way: " . $value);
         $type = TRUSTED_CGI_REQUEST;
+        $executor = \&execute_dumb_fetch_test;
         $$regression_tests[$si][$ri]{'expected-status-code'} = 200;
 
     } elsif ($token eq 'fetch test') {
 
         l(LL_FILE_LOADING, "URL to test in a dumb way: " . $value);
         $type = DUMB_FETCH_TEST;
         $$regression_tests[$si][$ri]{'expected-status-code'} = 200;
 
     } elsif ($token eq 'fetch test') {
 
         l(LL_FILE_LOADING, "URL to test in a dumb way: " . $value);
         $type = DUMB_FETCH_TEST;
+        $executor = \&execute_dumb_fetch_test;
         $$regression_tests[$si][$ri]{'expected-status-code'} = 200;
 
     } elsif ($token eq 'method test') {
 
         l(LL_FILE_LOADING, "Method to test: " . $value);
         $type = METHOD_TEST;
         $$regression_tests[$si][$ri]{'expected-status-code'} = 200;
 
     } elsif ($token eq 'method test') {
 
         l(LL_FILE_LOADING, "Method to test: " . $value);
         $type = METHOD_TEST;
+        $executor = \&execute_method_test;
         $$regression_tests[$si][$ri]{'expected-status-code'} = 200;
 
     } elsif ($token eq 'blocked url') {
 
         l(LL_FILE_LOADING, "URL to block-test: " . $value);
         $$regression_tests[$si][$ri]{'expected-status-code'} = 200;
 
     } elsif ($token eq 'blocked url') {
 
         l(LL_FILE_LOADING, "URL to block-test: " . $value);
+        $executor = \&execute_block_test;
         $type = BLOCK_TEST;
 
     } elsif ($token eq 'url') {
 
         l(LL_FILE_LOADING, "Sticky URL to test: " . $value);
         $type = STICKY_ACTIONS_TEST;
         $type = BLOCK_TEST;
 
     } elsif ($token eq 'url') {
 
         l(LL_FILE_LOADING, "Sticky URL to test: " . $value);
         $type = STICKY_ACTIONS_TEST;
+        $executor = \&execute_sticky_actions_test;
+
+    } elsif ($token eq 'redirected url') {
+
+        l(LL_FILE_LOADING, "Redirected URL to test: " . $value);
+        $type = REDIRECT_TEST;
+        $executor = \&execute_redirect_test;
 
     } else {
 
 
     } else {
 
@@ -293,6 +464,7 @@ sub enlist_new_test ($$$$$$) {
 
     $$regression_tests[$si][$ri]{'type'} = $type;
     $$regression_tests[$si][$ri]{'level'} = $type;
 
     $$regression_tests[$si][$ri]{'type'} = $type;
     $$regression_tests[$si][$ri]{'level'} = $type;
+    $$regression_tests[$si][$ri]{'executor'} = $executor;
 
     check_for_forbidden_characters($value);
 
 
     check_for_forbidden_characters($value);
 
@@ -306,7 +478,36 @@ sub enlist_new_test ($$$$$$) {
       "Regression test " . $number . " (section:" . $si . "):");
 }
 
       "Regression test " . $number . " (section:" . $si . "):");
 }
 
-sub load_action_files ($) {
+sub mark_matching_tests_for_skipping($) {
+    my $overwrite_condition = shift;
+
+    our @regression_tests;
+
+    for (my $s = 0;  $s < @regression_tests; $s++) {
+
+        my $r = 0;
+
+        while (defined $regression_tests[$s][$r]) {
+
+            if ($regression_tests[$s][$r]{'data'} eq $overwrite_condition) {
+                my $message = sprintf("Marking test %s for ignoring. Overwrite condition: %s.",
+                                      $regression_tests[$s][$r]{'number'}, $overwrite_condition);
+
+                l(LL_FILE_LOADING, $message);
+
+                # XXX: Should eventually get its own key so get_skip_reason()
+                #      can tell about the overwrite condition.
+                $regression_tests[$s][$r]{'ignore'} = 1;
+            }
+            $r++;
+        }
+    }
+}
+
+
+# XXX: Shares a lot of code with load_regression_tests_from_file()
+#      that should be factored out.
+sub load_action_files($) {
 
     # initialized here
     our %actions;
 
     # initialized here
     our %actions;
@@ -321,14 +522,12 @@ sub load_action_files ($) {
 
     my $ignored = 0;
 
 
     my $ignored = 0;
 
-    l(LL_STATUS, "Gathering regression tests from " .
-      @actionfiles . " action file(s) delivered by Privoxy.");
-
     for my $file_number (0 .. @actionfiles - 1) {
 
     for my $file_number (0 .. @actionfiles - 1) {
 
-        my $curl_url = ' "' . $actionfiles[$file_number] . '"';
+        my $curl_url = quote($actionfiles[$file_number]);
         my $actionfile = undef;
         my $sticky_actions = undef;
         my $actionfile = undef;
         my $sticky_actions = undef;
+        my $level_offset = 0;
 
         foreach (@{get_cgi_page_or_else($curl_url)}) {
 
 
         foreach (@{get_cgi_page_or_else($curl_url)}) {
 
@@ -348,6 +547,11 @@ sub load_action_files ($) {
             next unless defined $token;
 
             # Load regression tests
             next unless defined $token;
 
             # Load regression tests
+            if ($token eq 'default level offset') {
+
+                $level_offset = $value;
+                l(LL_FILE_LOADING, "Setting default level offset to " . $level_offset);
+            }
 
             if (token_starts_new_test($token)) {
 
 
             if (token_starts_new_test($token)) {
 
@@ -355,6 +559,10 @@ sub load_action_files ($) {
                 $ri++;
                 $count++;
                 enlist_new_test(\@regression_tests, $token, $value, $si, $ri, $count);
                 $ri++;
                 $count++;
                 enlist_new_test(\@regression_tests, $token, $value, $si, $ri, $count);
+                $no_checks = 1; # Already validated by enlist_new_test().
+                if ($level_offset != 0) {
+                    $regression_tests[$si][$ri]{'level'} += $level_offset;
+                }
             }
 
             if ($token =~ /level\s+(\d+)/i) {
             }
 
             if ($token =~ /level\s+(\d+)/i) {
@@ -372,7 +580,16 @@ sub load_action_files ($) {
                                 "action parameters are currently unsupported.");
                 }
             }
                                 "action parameters are currently unsupported.");
                 }
             }
-            
+
+            if ($token eq 'overwrite condition') {
+
+                l(LL_FILE_LOADING, "Detected overwrite condition: " . $value);
+                # We can only skip matching tests that have already
+                # be loaded but that is exactly what we want anyway.
+                mark_matching_tests_for_skipping($value);
+                next;
+            }
+
             if ($si == -1 || $ri == -1) {
                 # No beginning of a test detected yet,
                 # so we don't care about any other test
             if ($si == -1 || $ri == -1) {
                 # No beginning of a test detected yet,
                 # so we don't care about any other test
@@ -426,6 +643,11 @@ sub load_action_files ($) {
                 l(LL_FILE_LOADING, "Method: " . $value);
                 $regression_tests[$si][$ri]{'method'} = $value;
 
                 l(LL_FILE_LOADING, "Method: " . $value);
                 $regression_tests[$si][$ri]{'method'} = $value;
 
+            } elsif ($token eq 'redirect destination') {
+
+                l(LL_FILE_LOADING, "Redirect destination: " . $value);
+                $regression_tests[$si][$ri]{'redirect destination'} = $value;
+
             } elsif ($token eq 'url') {
 
                 if (defined $sticky_actions) {
             } elsif ($token eq 'url') {
 
                 if (defined $sticky_actions) {
@@ -435,17 +657,21 @@ sub load_action_files ($) {
                     l(LL_FILE_LOADING, "Sticky actions: " . $sticky_actions);
                     $regression_tests[$si][$ri]{'sticky-actions'} = $sticky_actions;
                 } else {
                     l(LL_FILE_LOADING, "Sticky actions: " . $sticky_actions);
                     $regression_tests[$si][$ri]{'sticky-actions'} = $sticky_actions;
                 } else {
-                    log_and_die("Sticky URL without Sticky Actions: $value");
+                    log_and_die("Sticky URL without Sticky Actions in $actionfile: $value");
                 }
 
             } else {
 
                 # We don't use it, so we don't need
                 $no_checks = 1;
                 }
 
             } else {
 
                 # We don't use it, so we don't need
                 $no_checks = 1;
+                l(LL_STATUS, "Enabling no_checks for $token") unless $no_checks;
+            }
+
+            # XXX: Necessary?
+            unless ($no_checks)  {
+                check_for_forbidden_characters($value);
+                check_for_forbidden_characters($token);
             }
             }
-            # XXX: Neccessary?
-            check_for_forbidden_characters($value) unless $no_checks;
-            check_for_forbidden_characters($token);
         }
     }
 
         }
     }
 
@@ -459,7 +685,17 @@ sub load_action_files ($) {
 #
 ############################################################################
 
 #
 ############################################################################
 
-sub execute_regression_tests () {
+# Fisher Yates shuffle from Perl's "How do I shuffle an array randomly?" FAQ
+sub fisher_yates_shuffle($) {
+    my $deck = shift;
+    my $i = @$deck;
+    while ($i--) {
+        my $j = int rand($i+1);
+        @$deck[$i,$j] = @$deck[$j,$i];
+    }
+}
+
+sub execute_regression_tests() {
 
     our @regression_tests;
     my $loops = get_cli_option('loops');
 
     our @regression_tests;
     my $loops = get_cli_option('loops');
@@ -482,14 +718,32 @@ sub execute_regression_tests () {
         my $failures;
         my $skipped = 0;
 
         my $failures;
         my $skipped = 0;
 
-        for my $s (0 .. @regression_tests - 1) {
+        if (cli_option_is_set('shuffle-tests')) {
+
+            # Shuffle both the test sections and
+            # the tests they contain.
+            #
+            # XXX: With the current data layout, shuffling tests
+            #      from different sections isn't possible.
+            #      Is this worth changing the layout?
+            fisher_yates_shuffle(\@regression_tests);
+            for (my $s = 0; $s < @regression_tests; $s++) {
+                fisher_yates_shuffle($regression_tests[$s]);
+            }
+        }
+
+        for (my $s = 0; $s < @regression_tests; $s++) {
 
             my $r = 0;
 
             while (defined $regression_tests[$s][$r]) {
 
 
             my $r = 0;
 
             while (defined $regression_tests[$s][$r]) {
 
-                die "Section id mismatch" if ($s != $regression_tests[$s][$r]{'section-id'});
-                die "Regression test id mismatch" if ($r != $regression_tests[$s][$r]{'regression-test-id'});
+                unless (cli_option_is_set('shuffle-tests')) {
+                    die "Section id mismatch" if ($s != $regression_tests[$s][$r]{'section-id'});
+                    die "Regression test id mismatch" if ($r != $regression_tests[$s][$r]{'regression-test-id'});
+                }
+                die "Internal error. Test executor missing."
+                    unless defined $regression_tests[$s][$r]{executor};
 
                 my $number = $regression_tests[$s][$r]{'number'};
                 my $skip_reason = get_skip_reason($regression_tests[$s][$r]);
 
                 my $number = $regression_tests[$s][$r]{'number'};
                 my $skip_reason = get_skip_reason($regression_tests[$s][$r]);
@@ -502,12 +756,13 @@ sub execute_regression_tests () {
 
                 } else {
 
 
                 } else {
 
-                    my $result = execute_regression_test($regression_tests[$s][$r]);
+                    my $result = $regression_tests[$s][$r]{executor}($regression_tests[$s][$r]);
 
                     log_result($regression_tests[$s][$r], $result, $tests);
 
                     $successes += $result;
                     $tests++;
 
                     log_result($regression_tests[$s][$r], $result, $tests);
 
                     $successes += $result;
                     $tests++;
+                    sleep(get_cli_option('sleep-time')) if (cli_option_is_set('sleep-time'));
                 }
                 $r++;
             }
                 }
                 $r++;
             }
@@ -529,7 +784,7 @@ sub execute_regression_tests () {
     }
 }
 
     }
 }
 
-sub get_skip_reason ($) {
+sub get_skip_reason($) {
     my $test = shift;
     my $skip_reason = undef;
 
     my $test = shift;
     my $skip_reason = undef;
 
@@ -550,7 +805,7 @@ sub get_skip_reason ($) {
     return $skip_reason;
 }
 
     return $skip_reason;
 }
 
-sub level_is_unacceptable ($) {
+sub level_is_unacceptable($) {
     my $level = shift;
     my $min_level = get_cli_option('min-level');
     my $max_level = get_cli_option('max-level');
     my $level = shift;
     my $min_level = get_cli_option('min-level');
     my $max_level = get_cli_option('max-level');
@@ -565,11 +820,11 @@ sub level_is_unacceptable ($) {
 
     } elsif ($level < $min_level) {
 
 
     } elsif ($level < $min_level) {
 
-        $reason = "Level to low (" . $level . " < " . $min_level . ")";
+        $reason = "Level too low (" . $level . " < " . $min_level . ")";
 
     } elsif ($level > $max_level) {
 
 
     } elsif ($level > $max_level) {
 
-        $reason = "Level to high (" . $level . " > " . $max_level . ")";
+        $reason = "Level too high (" . $level . " > " . $max_level . ")";
 
     } else {
 
 
     } else {
 
@@ -579,7 +834,7 @@ sub level_is_unacceptable ($) {
     return $reason;
 }
 
     return $reason;
 }
 
-sub dependency_unsatisfied ($) {
+sub dependency_unsatisfied($) {
 
     my $level = shift;
     our %dependencies;
 
     my $level = shift;
     our %dependencies;
@@ -620,7 +875,7 @@ sub dependency_unsatisfied ($) {
     return $dependency_problem;
 }
 
     return $dependency_problem;
 }
 
-sub register_dependency ($$) {
+sub register_dependency($$) {
 
     my $level = shift;
     my $dependency = shift;
 
     my $level = shift;
     my $dependency = shift;
@@ -640,70 +895,80 @@ sub register_dependency ($$) {
     }
 }
 
     }
 }
 
-# XXX: somewhat misleading name
-sub execute_regression_test ($) {
+sub execute_method_test($) {
 
     my $test = shift;
 
     my $test = shift;
-    my $result = 0;
-
-    if ($test->{'type'} == CLIENT_HEADER_TEST) {
-
-        $result = execute_client_header_regression_test($test);
-
-    } elsif ($test->{'type'} == SERVER_HEADER_TEST) {
-
-        $result = execute_server_header_regression_test($test);
-
-    } elsif ($test->{'type'} == DUMB_FETCH_TEST
-          or $test->{'type'} == TRUSTED_CGI_REQUEST) {
-
-        $result = execute_dumb_fetch_test($test);
-
-    } elsif ($test->{'type'} == METHOD_TEST) {
-
-        $result = execute_method_test($test);
-
-    } elsif ($test->{'type'} == BLOCK_TEST) {
+    our $privoxy_cgi_url;
 
 
-        $result = execute_block_test($test);
+    my $buffer_ref;
+    my $status_code;
+    my $method = $test->{'data'};
 
 
-    } elsif ($test->{'type'} == STICKY_ACTIONS_TEST) {
+    my $curl_parameters = '';
+    my $expected_status_code = $test->{'expected-status-code'};
 
 
-        $result = execute_sticky_actions_test($test);
+    $curl_parameters .= '--request ' . $method . ' ';
+    # Don't complain about the 'missing' body
+    $curl_parameters .= '--head ' if ($method =~ /^HEAD$/i);
 
 
-    } else {
+    $curl_parameters .= $privoxy_cgi_url;
 
 
-        die "Unsupported test type detected: " . $test->{'type'};
-    }
+    $buffer_ref = get_page_with_curl($curl_parameters);
+    $status_code = get_status_code($buffer_ref);
 
 
-    return $result;
+    return check_status_code_result($status_code, $expected_status_code);
 }
 
 }
 
-sub execute_method_test ($) {
+sub execute_redirect_test($) {
 
     my $test = shift;
     my $buffer_ref;
     my $status_code;
 
     my $test = shift;
     my $buffer_ref;
     my $status_code;
-    my $method = $test->{'data'};
 
     my $curl_parameters = '';
 
     my $curl_parameters = '';
-    my $expected_status_code = $test->{'expected-status-code'};
+    my $url = $test->{'data'};
+    my $redirect_destination;
+    my $expected_redirect_destination = $test->{'redirect destination'};
 
 
-    $curl_parameters .= '--request ' . $method . ' ';
-    # Don't complain about the 'missing' body
-    $curl_parameters .= '--head ' if ($method =~ /^HEAD$/i);
+    # XXX: Check if a redirect actually applies before doing the request.
+    #      otherwise the test may hit a real server in failure cases.
 
 
-    $curl_parameters .= PRIVOXY_CGI_URL;
+    $curl_parameters .= '--head ';
+
+    $curl_parameters .= quote($url);
 
     $buffer_ref = get_page_with_curl($curl_parameters);
     $status_code = get_status_code($buffer_ref);
 
 
     $buffer_ref = get_page_with_curl($curl_parameters);
     $status_code = get_status_code($buffer_ref);
 
-    return check_status_code_result($status_code, $expected_status_code);
+    if ($status_code ne "302") {
+        l(LL_VERBOSE_FAILURE,
+          "Ooops. Expected redirect to: '" . $expected_redirect_destination
+          . "' but got a response with status code: " . $status_code);
+        return 0;
+    }
+    foreach (@{$buffer_ref}) {
+        if (/^Location: (.*)\r\n/) {
+            $redirect_destination = $1;
+            last;
+        }
+    }
+
+    my $success = ($redirect_destination eq $expected_redirect_destination);
+
+    unless ($success) {
+        l(LL_VERBOSE_FAILURE,
+          "Ooops. Expected redirect to: '" . $expected_redirect_destination
+          . "' but the redirect leads to: '" . $redirect_destination. "'");
+    }
+
+    return $success;
 }
 
 }
 
-sub execute_dumb_fetch_test ($) {
+sub execute_dumb_fetch_test($) {
 
     my $test = shift;
 
     my $test = shift;
+    our $privoxy_cgi_url;
+
     my $buffer_ref;
     my $status_code;
 
     my $buffer_ref;
     my $status_code;
 
@@ -711,13 +976,13 @@ sub execute_dumb_fetch_test ($) {
     my $expected_status_code = $test->{'expected-status-code'};
 
     if (defined $test->{method}) {
     my $expected_status_code = $test->{'expected-status-code'};
 
     if (defined $test->{method}) {
-        $curl_parameters .= '--request ' . $test->{method} . ' ';
+        $curl_parameters .= '--request ' . quote($test->{method}) . ' ';
     }
     if ($test->{type} == TRUSTED_CGI_REQUEST) {
     }
     if ($test->{type} == TRUSTED_CGI_REQUEST) {
-        $curl_parameters .= '--referer ' . PRIVOXY_CGI_URL . ' ';
+        $curl_parameters .= '--referer ' . quote($privoxy_cgi_url) . ' ';
     }
 
     }
 
-    $curl_parameters .= $test->{'data'};
+    $curl_parameters .= quote($test->{'data'});
 
     $buffer_ref = get_page_with_curl($curl_parameters);
     $status_code = get_status_code($buffer_ref);
 
     $buffer_ref = get_page_with_curl($curl_parameters);
     $status_code = get_status_code($buffer_ref);
@@ -725,7 +990,7 @@ sub execute_dumb_fetch_test ($) {
     return check_status_code_result($status_code, $expected_status_code);
 }
 
     return check_status_code_result($status_code, $expected_status_code);
 }
 
-sub execute_block_test ($) {
+sub execute_block_test($) {
 
     my $test = shift;
     my $url = $test->{'data'};
 
     my $test = shift;
     my $url = $test->{'data'};
@@ -734,7 +999,7 @@ sub execute_block_test ($) {
     return defined $final_results->{'+block'};
 }
 
     return defined $final_results->{'+block'};
 }
 
-sub execute_sticky_actions_test ($) {
+sub execute_sticky_actions_test($) {
 
     my $test = shift;
     my $url = $test->{'data'};
 
     my $test = shift;
     my $url = $test->{'data'};
@@ -765,9 +1030,11 @@ sub execute_sticky_actions_test ($) {
     return $verified_actions == @sticky_actions;
 }
 
     return $verified_actions == @sticky_actions;
 }
 
-sub get_final_results ($) {
+sub get_final_results($) {
 
     my $url = shift;
 
     my $url = shift;
+    our $privoxy_cgi_url;
+
     my $curl_parameters = '';
     my %final_results = ();
     my $final_results_reached = 0;
     my $curl_parameters = '';
     my %final_results = ();
     my $final_results_reached = 0;
@@ -780,7 +1047,7 @@ sub get_final_results ($) {
     $url =~ s@:@%3A@g;
     $url =~ s@/@%2F@g;
 
     $url =~ s@:@%3A@g;
     $url =~ s@/@%2F@g;
 
-    $curl_parameters .= quote(PRIVOXY_CGI_URL . 'show-url-info?url=' . $url);
+    $curl_parameters .= quote($privoxy_cgi_url . 'show-url-info?url=' . $url);
 
     foreach (@{get_cgi_page_or_else($curl_parameters)}) {
 
 
     foreach (@{get_cgi_page_or_else($curl_parameters)}) {
 
@@ -789,7 +1056,10 @@ sub get_final_results ($) {
         next unless ($final_results_reached);
         last if (m@</td>@);
 
         next unless ($final_results_reached);
         last if (m@</td>@);
 
-        if (m@<br>([-+])<a.*>([^>]*)</a>(?: (\{.*\}))?@) {
+        # Privoxy versions before 3.0.16 add a space
+        # between action name and parameters, therefore
+        # the " ?".
+        if (m@<br>([-+])<a.*>([^>]*)</a>(?: ?(\{.*\}))?@) {
             my $action = $1.$2;
             my $parameter = $3;
             
             my $action = $1.$2;
             my $parameter = $3;
             
@@ -807,7 +1077,7 @@ sub get_final_results ($) {
     return \%final_results;
 }
 
     return \%final_results;
 }
 
-sub check_status_code_result ($$) {
+sub check_status_code_result($$) {
 
     my $status_code = shift;
     my $expected_status_code = shift;
 
     my $status_code = shift;
     my $expected_status_code = shift;
@@ -839,7 +1109,7 @@ sub check_status_code_result ($$) {
     return $result;
 }
 
     return $result;
 }
 
-sub execute_client_header_regression_test ($) {
+sub execute_client_header_regression_test($) {
 
     my $test = shift;
     my $buffer_ref;
 
     my $test = shift;
     my $buffer_ref;
@@ -852,7 +1122,7 @@ sub execute_client_header_regression_test ($) {
     return check_header_result($test, $header);
 }
 
     return check_header_result($test, $header);
 }
 
-sub execute_server_header_regression_test ($) {
+sub execute_server_header_regression_test($) {
 
     my $test = shift;
     my $buffer_ref;
 
     my $test = shift;
     my $buffer_ref;
@@ -865,12 +1135,12 @@ sub execute_server_header_regression_test ($) {
     return check_header_result($test, $header);
 }
 
     return check_header_result($test, $header);
 }
 
-sub interpret_result ($) {
+sub interpret_result($) {
     my $success = shift;
     return $success ? "Success" : "Failure";
 }
 
     my $success = shift;
     return $success ? "Success" : "Failure";
 }
 
-sub check_header_result ($$) {
+sub check_header_result($$) {
 
     my $test = shift;
     my $header = shift;
 
     my $test = shift;
     my $header = shift;
@@ -880,12 +1150,9 @@ sub check_header_result ($$) {
 
     if ($expect_header eq 'NO CHANGE') {
 
 
     if ($expect_header eq 'NO CHANGE') {
 
-        if (defined($header) and $header eq $test->{'data'}) {
-
-            $success = 1;
-
-        } else {
+        $success = (defined($header) and $header eq $test->{'data'});
 
 
+        unless ($success) {
             $header = "REMOVAL" unless defined $header;
             l(LL_VERBOSE_FAILURE,
               "Ooops. Got: '" . $header . "' while expecting: '" . $expect_header . "'");
             $header = "REMOVAL" unless defined $header;
             l(LL_VERBOSE_FAILURE,
               "Ooops. Got: '" . $header . "' while expecting: '" . $expect_header . "'");
@@ -893,26 +1160,20 @@ sub check_header_result ($$) {
 
     } elsif ($expect_header eq 'REMOVAL') {
 
 
     } elsif ($expect_header eq 'REMOVAL') {
 
-        if (defined($header) and $header eq $test->{'data'}) {
+        # XXX: Use more reliable check here and make sure
+        # the header has a different name.
+        $success = not (defined($header) and $header eq $test->{'data'});
 
 
+        unless ($success) {
             l(LL_VERBOSE_FAILURE,
               "Ooops. Expected removal but: '" . $header . "' is still there.");
             l(LL_VERBOSE_FAILURE,
               "Ooops. Expected removal but: '" . $header . "' is still there.");
-
-        } else {
-
-            # XXX: Use more reliable check here and make sure
-            # the header has a different name.
-            $success = 1;
         }
 
     } elsif ($expect_header eq 'SOME CHANGE') {
 
         }
 
     } elsif ($expect_header eq 'SOME CHANGE') {
 
-        if (defined($header) and not $header eq $test->{'data'}) {
-
-            $success = 1;
-
-        } else {
+        $success = (defined($header) and $header ne $test->{'data'});
 
 
+        unless  ($success) {
             $header = "REMOVAL" unless defined $header;
             l(LL_VERBOSE_FAILURE,
               "Ooops. Got: '" . $header . "' while expecting: SOME CHANGE");
             $header = "REMOVAL" unless defined $header;
             l(LL_VERBOSE_FAILURE,
               "Ooops. Got: '" . $header . "' while expecting: SOME CHANGE");
@@ -920,12 +1181,9 @@ sub check_header_result ($$) {
 
     } else {
 
 
     } else {
 
-        if (defined($header) and $header eq $expect_header) {
-
-            $success = 1;
-
-        } else {
+        $success = (defined($header) and $header eq $expect_header);
 
 
+        unless ($success) {
             $header = "No matching header" unless defined $header; # XXX: No header detected to be precise
             l(LL_VERBOSE_FAILURE,
               "Ooops. Got: '" . $header . "' while expecting: '" . $expect_header . "'");
             $header = "No matching header" unless defined $header; # XXX: No header detected to be precise
             l(LL_VERBOSE_FAILURE,
               "Ooops. Got: '" . $header . "' while expecting: '" . $expect_header . "'");
@@ -934,7 +1192,7 @@ sub check_header_result ($$) {
     return $success;
 }
 
     return $success;
 }
 
-sub get_header_name ($) {
+sub get_header_name($) {
 
     my $header = shift;
 
 
     my $header = shift;
 
@@ -943,7 +1201,7 @@ sub get_header_name ($) {
     return $header;
 }
 
     return $header;
 }
 
-sub get_header ($$) {
+sub get_header($$) {
 
     our $filtered_request = '';
 
 
     our $filtered_request = '';
 
@@ -965,7 +1223,7 @@ sub get_header ($$) {
 
     if ($expect_header eq 'REMOVAL'
      or $expect_header eq 'NO CHANGE'
 
     if ($expect_header eq 'REMOVAL'
      or $expect_header eq 'NO CHANGE'
-     or  $expect_header eq 'SOME CHANGE') {
+     or $expect_header eq 'SOME CHANGE') {
 
         $expect_header = $test->{'data'};
     }
 
         $expect_header = $test->{'data'};
     }
@@ -1003,7 +1261,7 @@ sub get_header ($$) {
     return $header;
 }
 
     return $header;
 }
 
-sub get_server_header ($$) {
+sub get_server_header($$) {
 
     my $buffer_ref = shift;
     my $test = shift;
 
     my $buffer_ref = shift;
     my $test = shift;
@@ -1041,13 +1299,23 @@ sub get_server_header ($$) {
     return $header;
 }
 
     return $header;
 }
 
-sub get_status_code ($) {
+sub get_status_code($) {
 
     my $buffer_ref = shift;
 
     my $buffer_ref = shift;
+    our $privoxy_cgi_url;
+
+    my $skip_connection_established_response = $privoxy_cgi_url =~ m@^https://@;
     my @buffer = @{$buffer_ref}; 
 
     foreach (@buffer) {
 
     my @buffer = @{$buffer_ref}; 
 
     foreach (@buffer) {
 
+        if ($skip_connection_established_response) {
+
+            next if (m@^HTTP/1\.1 200 Connection established@);
+            next if (m@^\r\n$@);
+            $skip_connection_established_response = 0;
+        }
+
         if (/^HTTP\/\d\.\d (\d{3})/) {
 
             return $1;
         if (/^HTTP\/\d\.\d (\d{3})/) {
 
             return $1;
@@ -1061,12 +1329,12 @@ sub get_status_code ($) {
     }
 }
 
     }
 }
 
-sub get_test_keys () {
+sub get_test_keys() {
     return ('tag', 'data', 'expect-header', 'ignore');
 }
 
 # XXX: incomplete
     return ('tag', 'data', 'expect-header', 'ignore');
 }
 
 # XXX: incomplete
-sub test_content_as_string ($) {
+sub test_content_as_string($) {
 
     my $test = shift;
 
 
     my $test = shift;
 
@@ -1107,7 +1375,7 @@ sub fuzz_header($) {
 #
 ############################################################################
 
 #
 ############################################################################
 
-sub get_cgi_page_or_else ($) {
+sub get_cgi_page_or_else($) {
 
     my $cgi_url = shift;
     my $content_ref = get_page_with_curl($cgi_url);
 
     my $cgi_url = shift;
     my $content_ref = get_page_with_curl($cgi_url);
@@ -1115,7 +1383,7 @@ sub get_cgi_page_or_else ($) {
 
     if (200 != $status_code) {
 
 
     if (200 != $status_code) {
 
-        my $log_message = "Failed to fetch Privoxy CGI Page. " .
+        my $log_message = "Failed to fetch Privoxy CGI page '$cgi_url'. " .
                           "Received status code ". $status_code .
                           " while only 200 is acceptable.";
 
                           "Received status code ". $status_code .
                           " while only 200 is acceptable.";
 
@@ -1134,7 +1402,7 @@ sub get_cgi_page_or_else ($) {
 }
 
 # XXX: misleading name
 }
 
 # XXX: misleading name
-sub get_show_request_with_curl ($) {
+sub get_show_request_with_curl($) {
 
     our $privoxy_cgi_url;
     my $test = shift;
 
     our $privoxy_cgi_url;
     my $test = shift;
@@ -1148,8 +1416,13 @@ sub get_show_request_with_curl ($) {
 
     # Enable the action to test
     $curl_parameters .= '-H \'X-Privoxy-Control: ' . $test->{'tag'} . '\' ';
 
     # Enable the action to test
     $curl_parameters .= '-H \'X-Privoxy-Control: ' . $test->{'tag'} . '\' ';
-    # The header to filter
-    $curl_parameters .= '-H \'' . $header . '\' ';
+
+    # Add the header to filter
+    if ($privoxy_cgi_url =~ m@^https://@ and $header =~ m@^Host:@) {
+        $curl_parameters .= '--proxy-header \'' . $header . '\' ';
+    } else {
+        $curl_parameters .= '-H \'' . $header . '\' ';
+    }
 
     $curl_parameters .= ' ';
     $curl_parameters .= $privoxy_cgi_url;
 
     $curl_parameters .= ' ';
     $curl_parameters .= $privoxy_cgi_url;
@@ -1158,7 +1431,7 @@ sub get_show_request_with_curl ($) {
     return get_cgi_page_or_else($curl_parameters);
 }
 
     return get_cgi_page_or_else($curl_parameters);
 }
 
-sub get_head_with_curl ($) {
+sub get_head_with_curl($) {
 
     our $fellatio_url = FELLATIO_URL;
     my $test = shift;
 
     our $fellatio_url = FELLATIO_URL;
     my $test = shift;
@@ -1177,7 +1450,7 @@ sub get_head_with_curl ($) {
     return get_page_with_curl($curl_parameters);
 }
 
     return get_page_with_curl($curl_parameters);
 }
 
-sub get_page_with_curl ($) {
+sub get_page_with_curl($) {
 
     our $proxy;
 
 
     our $proxy;
 
@@ -1187,8 +1460,9 @@ sub get_page_with_curl ($) {
     my $retries_left = get_cli_option('retries') + 1;
     my $failure_reason;
 
     my $retries_left = get_cli_option('retries') + 1;
     my $failure_reason;
 
-    $curl_line .= ' --proxy ' . $proxy if (defined $proxy);
-
+    if (defined $proxy) {
+        $curl_line .= ' --proxy ' . quote($proxy);
+    }
     # We want to see the HTTP status code
     $curl_line .= " --include ";
     # Let Privoxy emit two log messages less.
     # We want to see the HTTP status code
     $curl_line .= " --include ";
     # Let Privoxy emit two log messages less.
@@ -1202,6 +1476,8 @@ sub get_page_with_curl ($) {
     $curl_line .= " --user-agent '" . PRT_VERSION . "' ";
     # We aren't too patient
     $curl_line .= " --max-time '" . get_cli_option('max-time') . "' ";
     $curl_line .= " --user-agent '" . PRT_VERSION . "' ";
     # We aren't too patient
     $curl_line .= " --max-time '" . get_cli_option('max-time') . "' ";
+    # We don't want curl to treat "[]", "{}" etc. special
+    $curl_line .= " --globoff ";
 
     $curl_line .= $parameters;
     # XXX: still necessary?
 
     $curl_line .= $parameters;
     # XXX: still necessary?
@@ -1235,7 +1511,7 @@ sub get_page_with_curl ($) {
 #
 ############################################################################
 
 #
 ############################################################################
 
-sub array_as_string ($) {
+sub array_as_string($) {
     my $array_ref = shift;
     my $string = '';
 
     my $array_ref = shift;
     my $string = '';
 
@@ -1246,13 +1522,13 @@ sub array_as_string ($) {
     return $string;
 }
 
     return $string;
 }
 
-sub show_test ($) {
+sub show_test($) {
     my $test = shift;
     log_message('Test is:' . test_content_as_string($test));
 }
 
 # Conditional log
     my $test = shift;
     log_message('Test is:' . test_content_as_string($test));
 }
 
 # Conditional log
-sub l ($$) {
+sub l($$) {
     our $log_level;
     my $this_level = shift;
     my $message = shift;
     our $log_level;
     my $this_level = shift;
     my $message = shift;
@@ -1260,14 +1536,14 @@ sub l ($$) {
     log_message($message) if ($log_level & $this_level);
 }
 
     log_message($message) if ($log_level & $this_level);
 }
 
-sub log_and_die ($) {
+sub log_and_die($) {
     my $message = shift;
 
     log_message('Oh noes. ' . $message . ' Fatal error. Exiting.');
     exit;
 }
 
     my $message = shift;
 
     log_message('Oh noes. ' . $message . ' Fatal error. Exiting.');
     exit;
 }
 
-sub log_message ($) {
+sub log_message($) {
 
     my $message = shift;
 
 
     my $message = shift;
 
@@ -1284,7 +1560,7 @@ sub log_message ($) {
         if ($leading_log_date) {
             $year += 1900;
             $mon  += 1;
         if ($leading_log_date) {
             $year += 1900;
             $mon  += 1;
-            $time_stamp = sprintf("%i/%.2i/%.2i", $year, $mon, $mday);
+            $time_stamp = sprintf("%i-%.2i-%.2i", $year, $mon, $mday);
         }
 
         if ($leading_log_time) {
         }
 
         if ($leading_log_time) {
@@ -1295,101 +1571,122 @@ sub log_message ($) {
         $message = $time_stamp . ": " . $message;
     }
 
         $message = $time_stamp . ": " . $message;
     }
 
-    printf(STDERR "%s\n", $message);
+    printf("%s\n", $message);
 }
 
 }
 
-sub log_result ($$) {
+sub log_result($$) {
 
 
-    our $verbose_test_description;
     our $filtered_request;
 
     my $test = shift;
     my $result = shift;
     my $number = shift;
 
     our $filtered_request;
 
     my $test = shift;
     my $result = shift;
     my $number = shift;
 
-    my $message = '';
+    my $message = sprintf("%s for test %d",
+                          interpret_result($result),
+                          $test->{'number'});
 
 
-    $message .= interpret_result($result);
-    $message .= " for test ";
-    $message .= $number;
-    $message .= '/';
-    $message .= $test->{'number'};
-    $message .= '/';
-    $message .= $test->{'section-id'};
-    $message .= '/';
-    $message .= $test->{'regression-test-id'};
-    $message .= '.';
+    if (cli_option_is_set('verbose')) {
+        $message .= sprintf(" (%d/%d/%d)", $number,
+                            $test->{'section-id'},
+                            $test->{'regression-test-id'});
+    }
 
 
-    if ($verbose_test_description) {
+    $message .= '. ';
 
 
-        if ($test->{'type'} == CLIENT_HEADER_TEST) {
+    if ($test->{'type'} == CLIENT_HEADER_TEST) {
 
 
-            $message .= ' Header ';
-            $message .= quote($test->{'data'});
-            $message .= ' and tag ';
-            $message .= quote($test->{'tag'});
+        $message .= 'Header ';
+        $message .= quote($test->{'data'});
+        $message .= ' and tag ';
+        $message .= quote($test->{'tag'});
 
 
-        } elsif ($test->{'type'} == SERVER_HEADER_TEST) {
+    } elsif ($test->{'type'} == SERVER_HEADER_TEST) {
 
 
-            $message .= ' Request Header ';
-            $message .= quote($test->{'data'});
-            $message .= ' and tag ';
-            $message .= quote($test->{'tag'});
+        $message .= 'Request Header ';
+        $message .= quote($test->{'data'});
+        $message .= ' and tag ';
+        $message .= quote($test->{'tag'});
 
 
-        } elsif ($test->{'type'} == DUMB_FETCH_TEST) {
+    } elsif ($test->{'type'} == DUMB_FETCH_TEST) {
 
 
-            $message .= ' URL ';
-            $message .= quote($test->{'data'});
-            $message .= ' and expected status code ';
-            $message .= quote($test->{'expected-status-code'});
+        $message .= 'URL ';
+        $message .= quote($test->{'data'});
+        $message .= ' and expected status code ';
+        $message .= quote($test->{'expected-status-code'});
 
 
-        } elsif ($test->{'type'} == TRUSTED_CGI_REQUEST) {
+    } elsif ($test->{'type'} == TRUSTED_CGI_REQUEST) {
 
 
-            $message .= ' CGI URL ';
-            $message .= quote($test->{'data'});
-            $message .= ' and expected status code ';
-            $message .= quote($test->{'expected-status-code'});
+        $message .= 'CGI URL ';
+        $message .= quote($test->{'data'});
+        $message .= ' and expected status code ';
+        $message .= quote($test->{'expected-status-code'});
 
 
-        } elsif ($test->{'type'} == METHOD_TEST) {
+    } elsif ($test->{'type'} == METHOD_TEST) {
 
 
-            $message .= ' HTTP method ';
-            $message .= quote($test->{'data'});
-            $message .= ' and expected status code ';
-            $message .= quote($test->{'expected-status-code'});
+        $message .= 'HTTP method ';
+        $message .= quote($test->{'data'});
+        $message .= ' and expected status code ';
+        $message .= quote($test->{'expected-status-code'});
 
 
-        } elsif ($test->{'type'} == BLOCK_TEST) {
+    } elsif ($test->{'type'} == BLOCK_TEST) {
 
 
-            $message .= ' Supposedly-blocked URL: ';
-            $message .= quote($test->{'data'});
+        $message .= 'Supposedly-blocked URL: ';
+        $message .= quote($test->{'data'});
 
 
-        } elsif ($test->{'type'} == STICKY_ACTIONS_TEST) {
+    } elsif ($test->{'type'} == STICKY_ACTIONS_TEST) {
 
 
-            $message .= ' Sticky Actions: ';
-            $message .= quote($test->{'sticky-actions'});
-            $message .= ' and URL: ';
-            $message .= quote($test->{'data'});
+        $message .= 'Sticky Actions: ';
+        $message .= quote($test->{'sticky-actions'});
+        $message .= ' and URL: ';
+        $message .= quote($test->{'data'});
 
 
-        } else {
+    } elsif ($test->{'type'} == REDIRECT_TEST) {
 
 
-            die "Incomplete support for test type " . $test->{'type'} .  " detected.";
-        }
+        $message .= 'Redirected URL: ';
+        $message .= quote($test->{'data'});
+        $message .= ' and redirect destination: ';
+        $message .= quote($test->{'redirect destination'});
+
+    } else {
+
+        die "Incomplete support for test type " . $test->{'type'} .  " detected.";
     }
 
     log_message($message) if (!$result or cli_option_is_set('verbose'));
 }
 
     }
 
     log_message($message) if (!$result or cli_option_is_set('verbose'));
 }
 
-sub quote ($) {
+sub quote($) {
     my $s = shift;
     return '\'' . $s . '\'';
 }
 
     my $s = shift;
     return '\'' . $s . '\'';
 }
 
-sub print_version () {
-    printf PRT_VERSION . "\n" . 'Copyright (C) 2007-2009 Fabian Keil <fk@fabiankeil.de>' . "\n";
+sub print_version() {
+    printf PRT_VERSION . "\n";
 }
 
 }
 
-sub help () {
+sub list_test_types() {
+    my %test_types = (
+        'Client header test'  => CLIENT_HEADER_TEST,
+        'Server header test'  =>  2,
+        'Dumb fetch test'     =>  3,
+        'Method test'         =>  4,
+        'Sticky action test'  =>  5,
+        'Trusted CGI test'    =>  6,
+        'Block test'          =>  7,
+        'Redirect test'       => 108,
+    );
+
+    print "\nThe supported test types and their default levels are:\n";
+    foreach my $test_type (sort { $test_types{$a} <=> $test_types{$b} } keys %test_types) {
+        printf "     %-20s -> %3.d\n", $test_type, $test_types{$test_type};
+    }
+}
+
+sub help() {
 
     our %cli_options;
 
     our %cli_options;
+    our $privoxy_cgi_url;
 
     print_version();
 
 
     print_version();
 
@@ -1403,26 +1700,39 @@ Options and their default values if they have any:
     [--help]
     [--header-fuzzing]
     [--level]
     [--help]
     [--header-fuzzing]
     [--level]
+    [--local-test-file]
     [--loops $cli_options{'loops'}]
     [--max-level $cli_options{'max-level'}]
     [--max-time $cli_options{'max-time'}]
     [--min-level $cli_options{'min-level'}]
     [--loops $cli_options{'loops'}]
     [--max-level $cli_options{'max-level'}]
     [--max-time $cli_options{'max-time'}]
     [--min-level $cli_options{'min-level'}]
-    [--privoxy-address]
+    [--privoxy-address $cli_options{'privoxy-address'}]
+    [--privoxy-cgi-prefix $privoxy_cgi_url]
     [--retries $cli_options{'retries'}]
     [--show-skipped-tests]
     [--retries $cli_options{'retries'}]
     [--show-skipped-tests]
+    [--shuffle-tests]
+    [--sleep-time $cli_options{'sleep-time'}]
     [--test-number]
     [--verbose]
     [--version]
     [--test-number]
     [--verbose]
     [--version]
-see "perldoc $0" for more information
     EOF
     ;
     EOF
     ;
+
+    list_test_types();
+
+    print << "    EOF"
+
+Try "perldoc $0" for more information
+    EOF
+    ;
+
     exit(0);
 }
 
     exit(0);
 }
 
-sub init_cli_options () {
+sub init_cli_options() {
 
     our %cli_options;
     our $log_level;
 
     our %cli_options;
     our $log_level;
+    our $proxy;
 
     $cli_options{'debug'}     = $log_level;
     $cli_options{'forks'}     = CLI_FORKS;
 
     $cli_options{'debug'}     = $log_level;
     $cli_options{'forks'}     = CLI_FORKS;
@@ -1430,39 +1740,46 @@ sub init_cli_options () {
     $cli_options{'max-level'} = CLI_MAX_LEVEL;
     $cli_options{'max-time'}  = CLI_MAX_TIME;
     $cli_options{'min-level'} = CLI_MIN_LEVEL;
     $cli_options{'max-level'} = CLI_MAX_LEVEL;
     $cli_options{'max-time'}  = CLI_MAX_TIME;
     $cli_options{'min-level'} = CLI_MIN_LEVEL;
+    $cli_options{'sleep-time'}= CLI_SLEEP_TIME;
     $cli_options{'retries'}   = CLI_RETRIES;
     $cli_options{'retries'}   = CLI_RETRIES;
+    $cli_options{'privoxy-address'} = $proxy;
 }
 
 }
 
-sub parse_cli_options () {
+sub parse_cli_options() {
 
     our %cli_options;
     our $log_level;
 
     our %cli_options;
     our $log_level;
+    our $privoxy_cgi_url;
 
     init_cli_options();
 
     GetOptions (
 
     init_cli_options();
 
     GetOptions (
-        'debug=s'            => \$cli_options{'debug'},
-        'forks=s'            => \$cli_options{'forks'},
+        'debug=i'            => \$cli_options{'debug'},
+        'forks=i'            => \$cli_options{'forks'},
         'fuzzer-address=s'   => \$cli_options{'fuzzer-address'},
         'fuzzer-feeding'     => \$cli_options{'fuzzer-feeding'},
         'header-fuzzing'     => \$cli_options{'header-fuzzing'},
         'fuzzer-address=s'   => \$cli_options{'fuzzer-address'},
         'fuzzer-feeding'     => \$cli_options{'fuzzer-feeding'},
         'header-fuzzing'     => \$cli_options{'header-fuzzing'},
-        'help'               => sub {help},
-        'level=s'            => \$cli_options{'level'},
-        'loops=s'            => \$cli_options{'loops'},
-        'max-level=s'        => \$cli_options{'max-level'},
-        'max-time=s'         => \$cli_options{'max-time'},
-        'min-level=s'        => \$cli_options{'min-level'},
+        'help'               => \&help,
+        'level=i'            => \$cli_options{'level'},
+        'local-test-file=s'  => \$cli_options{'local-test-file'},
+        'loops=i'            => \$cli_options{'loops'},
+        'max-level=i'        => \$cli_options{'max-level'},
+        'max-time=i'         => \$cli_options{'max-time'},
+        'min-level=i'        => \$cli_options{'min-level'},
         'privoxy-address=s'  => \$cli_options{'privoxy-address'},
         'privoxy-address=s'  => \$cli_options{'privoxy-address'},
-        'retries=s'          => \$cli_options{'retries'},
+        'privoxy-cgi-prefix=s' => \$privoxy_cgi_url, # XXX: Should use cli_options()
+        'retries=i'          => \$cli_options{'retries'},
+        'shuffle-tests'      => \$cli_options{'shuffle-tests'},
         'show-skipped-tests' => \$cli_options{'show-skipped-tests'},
         'show-skipped-tests' => \$cli_options{'show-skipped-tests'},
-        'test-number=s'      => \$cli_options{'test-number'},
+        'sleep-time=i'       => \$cli_options{'sleep-time'},
+        'test-number=i'      => \$cli_options{'test-number'},
         'verbose'            => \$cli_options{'verbose'},
         'version'            => sub {print_version && exit(0)}
         'verbose'            => \$cli_options{'verbose'},
         'version'            => sub {print_version && exit(0)}
-    );
+    ) or exit(1);
     $log_level |= $cli_options{'debug'};
 }
 
     $log_level |= $cli_options{'debug'};
 }
 
-sub cli_option_is_set ($) {
+sub cli_option_is_set($) {
 
     our %cli_options;
     my $cli_option = shift;
 
     our %cli_options;
     my $cli_option = shift;
@@ -1470,7 +1787,7 @@ sub cli_option_is_set ($) {
     return defined $cli_options{$cli_option};
 }
 
     return defined $cli_options{$cli_option};
 }
 
-sub get_cli_option ($) {
+sub get_cli_option($) {
 
     our %cli_options;
     my $cli_option = shift;
 
     our %cli_options;
     my $cli_option = shift;
@@ -1511,12 +1828,12 @@ sub start_forks($) {
     }
 }
 
     }
 }
 
-sub main () {
+sub main() {
 
     init_our_variables();
     parse_cli_options();
     init_proxy_settings('vanilla-proxy');
 
     init_our_variables();
     parse_cli_options();
     init_proxy_settings('vanilla-proxy');
-    load_regressions_tests();
+    load_regression_tests();
     init_proxy_settings('fuzz-proxy');
     start_forks(get_cli_option('forks')) if cli_option_is_set('forks');
     execute_regression_tests();
     init_proxy_settings('fuzz-proxy');
     start_forks(get_cli_option('forks')) if cli_option_is_set('forks');
     execute_regression_tests();
@@ -1532,10 +1849,10 @@ B<privoxy-regression-test> - A regression test "framework" for Privoxy.
 
 B<privoxy-regression-test> [B<--debug bitmask>] [B<--forks> forks]
 [B<--fuzzer-feeding>] [B<--fuzzer-feeding>] [B<--help>] [B<--level level>]
 
 B<privoxy-regression-test> [B<--debug bitmask>] [B<--forks> forks]
 [B<--fuzzer-feeding>] [B<--fuzzer-feeding>] [B<--help>] [B<--level level>]
-[B<--loops count>] [B<--max-level max-level>] [B<--max-time max-time>]
-[B<--min-level min-level>] B<--privoxy-address proxy-address>
-[B<--retries retries>] [B<--test-number test-number>]
-[B<--show-skipped-tests>] [B<--verbose>]
+[B<--local-test-file testfile>] [B<--loops count>] [B<--max-level max-level>]
+[B<--max-time max-time>] [B<--min-level min-level>] B<--privoxy-address proxy-address>
+B<--privoxy-cgi-prefix cgi-prefix> [B<--retries retries>] [B<--test-number test-number>]
+[B<--show-skipped-tests>] [B<--sleep-time> seconds] [B<--verbose>]
 [B<--version>]
 
 =head1 DESCRIPTION
 [B<--version>]
 
 =head1 DESCRIPTION
@@ -1619,6 +1936,41 @@ To verify that a specific set of actions is applied to an URL, use:
 The sticky actions will be checked for all URLs below it
 until the next sticky actions directive.
 
 The sticky actions will be checked for all URLs below it
 until the next sticky actions directive.
 
+To verify that requests for a URL get redirected, use:
+
+    # Redirected URL = http://www.example.com/redirect-me
+    # Redirect Destination = http://www.example.org/redirected
+
+To skip a test, add the following line:
+
+    # Ignore = Yes
+
+The difference between a skipped test and a removed one is that removing
+a test affects the numbers of the following tests, while a skipped test
+is still loaded and thus keeps the test numbers unchanged.
+
+Sometimes user modifications intentionally conflict with tests in the
+default configuration and thus cause test failures. Adding the Ignore
+directive to the failing tests works but is inconvenient as the directive
+is likely to get lost with the next update.
+
+Overwrite conditions are an alternative and can be added in any action
+file as long as the come after the test that is expected to fail.
+They cause all previous tests that match the condition to be skipped.
+
+It is recommended to put the overwrite condition below the custom Privoxy
+section that causes the expected test failure and before the custom test
+that verifies that tests the now expected behaviour. Example:
+
+    # The following section is expected to overwrite a section in
+    # default.action, whose effect is being tested. Thus also disable
+    # the test that is now expected to fail and add a new one.
+    #
+    {+block{Facebook makes Firefox even more unstable. Do not want.}}
+    # Overwrite condition = http://apps.facebook.com/onthefarm/track.php?creative=&cat=friendvisit&subcat=weeds&key=a789a971dc687bee4c20c044834fabdd&next=index.php%3Fref%3Dnotif%26visitId%3D898835505
+    # Blocked URL = http://apps.facebook.com/
+    .facebook./
+
 =head1 TEST LEVELS
 
 All tests have test levels to let the user
 =head1 TEST LEVELS
 
 All tests have test levels to let the user
@@ -1626,9 +1978,22 @@ control which ones to execute (see I<OPTIONS> below).
 Test levels are either set with the B<Level> directive,
 or implicitly through the test type.
 
 Test levels are either set with the B<Level> directive,
 or implicitly through the test type.
 
-Block tests default to level 7, fetch tests to level 6,
-"Sticky Actions" tests default to level 5, tests for trusted CGI
-requests to level 3 and client-header-action tests to level 1.
+Redirect tests default to level 108, block tests to level 7,
+fetch tests to level 6, "Sticky Actions" tests default to
+level 5, tests for trusted CGI requests to level 3 and
+client-header-action tests to level 1.
+
+The current redirect test level is above the default
+max-level value as failed tests will result in outgoing
+connections. Use the B<--max-level> option to run them
+as well.
+
+The "Default level offset" directive can be used to change
+the default level by a given value. This directive affects
+all tests located after it until the end of the file or a another
+"Default level offset" directive is reached. The purpose of this
+directive is to make it more convenient to skip similar tests in
+a given file without having to remove or disable the tests completely.
 
 =head1 OPTIONS
 
 
 =head1 OPTIONS
 
@@ -1657,6 +2022,12 @@ headers in a way that should not affect the test result.
 
 B<--level level> Only execute tests with the specified B<level>. 
 
 
 B<--level level> Only execute tests with the specified B<level>. 
 
+B<--local-test-file test-file> Do not get the tests
+through Privoxy's web interface, but use a single local
+file. Not recommended for testing Privoxy, but can be useful
+to "misappropriate" Privoxy-Regression-Test to test other
+stuff, like webserver configurations.
+
 B<--loop count> Loop through the regression tests B<count> times. 
 Useful to feed a fuzzer, or when doing stress tests with
 several Privoxy-Regression-Test instances running at the same
 B<--loop count> Loop through the regression tests B<count> times. 
 Useful to feed a fuzzer, or when doing stress tests with
 several Privoxy-Regression-Test instances running at the same
@@ -1676,8 +2047,23 @@ above or equal to the numerical B<min-level>.
 
 B<--privoxy-address proxy-address> Privoxy's listening address.
 If it's not set, the value of the environment variable http_proxy
 
 B<--privoxy-address proxy-address> Privoxy's listening address.
 If it's not set, the value of the environment variable http_proxy
-will be used. B<proxy-address> has to be specified in http_proxy
-syntax.
+will be used unless the variable isn't set in which case
+http://127.0.0.1:8118/ will be used. B<proxy-address> has to
+be specified in http_proxy syntax.
+
+B<--privoxy-cgi-prefix privoxy-cgi-prefix> The prefix to use when
+building URLs that are supposed to reach Privoxy's CGI interface.
+If it's not set, B<http://p.p/> is used, which is supposed to work
+with the default Privoxy configuration.
+If Privoxy has been built with B<FEATURE_HTTPS_INSPECTION> enabled,
+and if https inspection is activated with the B<+https-inspection>
+action, this option can be used with
+B<https://p.p/> provided the system running Privoxy-Regression-Test
+has been configured to trust the certificate used by Privoxy.
+Note that there are currently two tests in the official
+B<regression-tests.action> file that are expected to fail when
+using a B<privoxy-cgi-prefix> with B<https://> and aren't automatically
+skipped.
 
 B<--retries retries> Retry B<retries> times.
 
 
 B<--retries retries> Retry B<retries> times.
 
@@ -1686,7 +2072,15 @@ number.
 
 B<--show-skipped-tests> Log skipped tests even if verbose mode is off.
 
 
 B<--show-skipped-tests> Log skipped tests even if verbose mode is off.
 
-B<--verbose> Log succesful tests as well. By default only
+B<--shuffle-tests> Shuffle test sections and their tests before
+executing them. When combined with B<--forks>, this can increase
+the chances of detecting race conditions. Of course some problems
+are easier to detect without this option.
+
+B<--sleep-time seconds> Wait B<seconds> between tests. Useful when
+debugging issues with systems that don't log with millisecond precision.
+
+B<--verbose> Log successful tests as well. By default only
 the failures are logged.
 
 B<--version> Print version and exit.
 the failures are logged.
 
 B<--version> Print version and exit.