3 ############################################################################
5 # Privoxy-Regression-Test
7 # A regression test "framework" for Privoxy. For documentation see:
8 # perldoc privoxy-regression-test.pl
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?
20 # Copyright (c) 2007-2024 Fabian Keil <fk@fabiankeil.de>
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.
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.
34 ############################################################################
41 PRT_VERSION => 'Privoxy-Regression-Test 0.7.5',
50 # The reason for a maximum test level is explained in the
51 # perldoc section TEST LEVELS near the end of this file.
56 PRIVOXY_ADDRESS => 'http://127.0.0.1:8118/',
57 PRIVOXY_CGI_URL => 'http://p.p/',
58 FELLATIO_URL => 'http://127.0.0.1:8080/',
59 LEADING_LOG_DATE => 1,
60 LEADING_LOG_TIME => 1,
62 DEBUG_LEVEL_FILE_LOADING => 0,
63 DEBUG_LEVEL_PAGE_FETCHING => 0,
64 DEBUG_LEVEL_VERBOSE_FAILURE => 1,
65 # XXX: Only partly implemented and mostly useless.
66 DEBUG_LEVEL_VERBOSE_SUCCESS => 0,
67 DEBUG_LEVEL_STATUS => 1,
69 # Internal use, don't modify
70 # Available debug bits:
72 LL_VERBOSE_FAILURE => 2,
73 LL_PAGE_FETCHING => 4,
75 LL_VERBOSE_SUCCESS => 16,
78 CLIENT_HEADER_TEST => 1,
79 SERVER_HEADER_TEST => 2,
82 STICKY_ACTIONS_TEST => 5,
83 TRUSTED_CGI_REQUEST => 6,
88 sub init_our_variables() {
90 our $leading_log_time = LEADING_LOG_TIME;
91 our $leading_log_date = LEADING_LOG_DATE;
92 our $privoxy_cgi_url = PRIVOXY_CGI_URL;
93 our $log_level = get_default_log_level();
94 our $proxy = defined $ENV{'http_proxy'} ? $ENV{'http_proxy'} : PRIVOXY_ADDRESS;
97 sub get_default_log_level() {
101 $log_level |= LL_FILE_LOADING if DEBUG_LEVEL_FILE_LOADING;
102 $log_level |= LL_PAGE_FETCHING if DEBUG_LEVEL_PAGE_FETCHING;
103 $log_level |= LL_VERBOSE_FAILURE if DEBUG_LEVEL_VERBOSE_FAILURE;
104 $log_level |= LL_VERBOSE_SUCCESS if DEBUG_LEVEL_VERBOSE_SUCCESS;
105 $log_level |= LL_STATUS if DEBUG_LEVEL_STATUS;
107 # This one is supposed to be always on.
108 $log_level |= LL_SOFT_ERROR;
113 ############################################################################
115 # File loading functions
117 ############################################################################
125 # Unescape brackets and dots
126 $tag =~ s@\\(?=[{}().+])@@g;
128 # log_message("Parsed tag: " . $tag);
130 check_for_forbidden_characters($tag);
135 sub check_for_forbidden_characters($) {
138 my $allowed = '[-=\dA-Za-z~{}\[\]:./();\t ,+@"_%?&*^|]';
140 unless ($string =~ m/^$allowed*$/o) {
141 my $forbidden = $string;
142 $forbidden =~ s@^$allowed*(.).*@$1@;
144 log_and_die("'" . $string . "' contains character '" . $forbidden. "' which is unacceptable.");
148 sub load_regression_tests() {
149 if (cli_option_is_set('local-test-file')) {
150 load_regression_tests_from_file(get_cli_option('local-test-file'));
152 load_regression_tests_through_privoxy();
156 # XXX: Contains a lot of code duplicated from load_action_files()
157 # that should be factored out.
158 sub load_regression_tests_from_file($) {
159 my $action_file = shift;
163 our @regression_tests;
165 my $si = 0; # Section index
166 my $ri = -1; # Regression test index
171 my $sticky_actions = undef;
173 l(LL_STATUS, "Gathering regression tests from local file " . $action_file);
175 open(my $ACTION_FILE, "<", $action_file)
176 or log_and_die("Failed to open $action_file: $!");
178 while (<$ACTION_FILE>) {
182 my ($token, $value) = tokenize($_);
184 next unless defined $token;
186 # Load regression tests
188 if (token_starts_new_test($token)) {
190 # Beginning of new regression test.
193 enlist_new_test(\@regression_tests, $token, $value, $si, $ri, $count);
194 $no_checks = 1; # Already validated by enlist_new_test().
197 if ($token =~ /level\s+(\d+)/i) {
200 register_dependency($level, $value);
203 if ($token eq 'sticky actions') {
205 # Will be used by each following Sticky URL.
206 $sticky_actions = $value;
207 if ($sticky_actions =~ /{[^}]*\s/) {
208 log_and_die("'Sticky Actions' with whitespace inside the " .
209 "action parameters are currently unsupported.");
213 if ($si == -1 || $ri == -1) {
214 # No beginning of a test detected yet,
215 # so we don't care about any other test
220 if ($token eq 'expect header') {
222 l(LL_FILE_LOADING, "Detected expectation: " . $value);
223 $regression_tests[$si][$ri]{'expect-header'} = $value;
225 } elsif ($token eq 'tag') {
229 my $tag = parse_tag($value);
231 # We already checked in parse_tag() after filtering
234 l(LL_FILE_LOADING, "Detected TAG: " . $tag);
236 # Save tag for all tests in this section
238 $regression_tests[$si][$ri]{'tag'} = $tag;
244 } elsif ($token eq 'ignore' && $value =~ /Yes/i) {
246 l(LL_FILE_LOADING, "Ignoring section: " . test_content_as_string($regression_tests[$si][$ri]));
247 $regression_tests[$si][$ri]{'ignore'} = 1;
250 } elsif ($token eq 'expect status code') {
252 l(LL_FILE_LOADING, "Expecting status code: " . $value);
253 $regression_tests[$si][$ri]{'expected-status-code'} = $value;
255 } elsif ($token eq 'level') { # XXX: stupid name
257 $value =~ s@(\d+).*@$1@;
258 l(LL_FILE_LOADING, "Level: " . $value);
259 $regression_tests[$si][$ri]{'level'} = $value;
261 } elsif ($token eq 'method') {
263 l(LL_FILE_LOADING, "Method: " . $value);
264 $regression_tests[$si][$ri]{'method'} = $value;
266 } elsif ($token eq 'redirect destination') {
268 l(LL_FILE_LOADING, "Redirect destination: " . $value);
269 $regression_tests[$si][$ri]{'redirect destination'} = $value;
271 } elsif ($token eq 'url') {
273 if (defined $sticky_actions) {
274 die "WTF? Attempted to overwrite Sticky Actions"
275 if defined ($regression_tests[$si][$ri]{'sticky-actions'});
277 l(LL_FILE_LOADING, "Sticky actions: " . $sticky_actions);
278 $regression_tests[$si][$ri]{'sticky-actions'} = $sticky_actions;
280 log_and_die("Sticky URL without Sticky Actions in $action_file: $value");
285 # We don't use it, so we don't need
287 l(LL_STATUS, "Enabling no_checks for $token") unless $no_checks;
291 unless ($no_checks) {
292 check_for_forbidden_characters($value);
293 check_for_forbidden_characters($token);
297 l(LL_FILE_LOADING, "Done loading " . $count . " regression tests."
298 . " Of which " . $ignored. " will be ignored)\n");
303 sub load_regression_tests_through_privoxy() {
305 our $privoxy_cgi_url;
307 our %privoxy_features;
312 my $privoxy_version = '(Unknown version!)';
314 $curl_url .= $privoxy_cgi_url;
315 $curl_url .= 'show-status';
317 l(LL_STATUS, "Asking Privoxy for the number of action files available ...");
319 # Dear Privoxy, please reload the config file if necessary ...
320 get_cgi_page_or_else($curl_url);
322 # ... so we get the latest one here.
323 foreach (@{get_cgi_page_or_else($curl_url)}) {
326 if (/<td>(.*?)<\/td><td class=\"buttons\"><a href=\"\/show-status\?file=actions&index=(\d+)\">/) {
328 my $url = $privoxy_cgi_url . 'show-status?file=actions&index=' . $2;
329 $actionfiles[$file_number++] = $url;
331 } elsif (m@config\.html#.*\">([^<]*)</a>\s+(.*)<br>@) {
333 my $directive = $1 . " " . $2;
334 push (@privoxy_config, $directive);
336 } elsif (m@<td><code>([^<]*)</code></td>@) {
340 } elsif (m@<td> (Yes|No) </td>@) {
342 $privoxy_features{$feature} = $1 if defined $feature;
345 } elsif (m@This is <a href="https?://www.privoxy.org/">Privoxy</a> (\d+\.\d+\.\d+) on@) {
346 $privoxy_version = $1;
350 l(LL_STATUS, "Gathering regression tests from " .
351 @actionfiles . " action file(s) delivered by Privoxy $privoxy_version.");
353 load_action_files(\@actionfiles);
356 sub token_starts_new_test($) {
359 my @new_test_directives = ('set header', 'fetch test',
360 'trusted cgi request', 'request header', 'method test',
361 'blocked url', 'url', 'redirected url');
363 foreach my $new_test_directive (@new_test_directives) {
364 return 1 if $new_test_directive eq $token;
372 my ($token, $value) = (undef, undef);
374 # Remove leading and trailing white space and a
375 # leading <pre> which is part of the first line.
379 # Reverse HTML-encoding
380 # XXX: Seriously incomplete.
385 if (/^\#\s*([^=:#]*?)\s*[=]\s*([^#]+)(?:#.*)?$/) {
390 $token =~ s@\s\s+@ @g;
391 $token =~ tr/[A-Z]/[a-z]/;
393 } elsif (/^TAG\s*:(.*)$/) {
399 return ($token, $value);
402 sub enlist_new_test($$$$$$) {
404 my ($regression_tests, $token, $value, $si, $ri, $number) = @_;
408 if ($token eq 'set header') {
410 l(LL_FILE_LOADING, "Header to set: " . $value);
411 $type = CLIENT_HEADER_TEST;
412 $executor = \&execute_client_header_regression_test;
414 } elsif ($token eq 'request header') {
416 l(LL_FILE_LOADING, "Header to request: " . $value);
417 $type = SERVER_HEADER_TEST;
418 $executor = \&execute_server_header_regression_test;
419 $$regression_tests[$si][$ri]{'expected-status-code'} = 200;
421 } elsif ($token eq 'trusted cgi request') {
423 l(LL_FILE_LOADING, "CGI URL to test in a dumb way: " . $value);
424 $type = TRUSTED_CGI_REQUEST;
425 $executor = \&execute_dumb_fetch_test;
426 $$regression_tests[$si][$ri]{'expected-status-code'} = 200;
428 } elsif ($token eq 'fetch test') {
430 l(LL_FILE_LOADING, "URL to test in a dumb way: " . $value);
431 $type = DUMB_FETCH_TEST;
432 $executor = \&execute_dumb_fetch_test;
433 $$regression_tests[$si][$ri]{'expected-status-code'} = 200;
435 } elsif ($token eq 'method test') {
437 l(LL_FILE_LOADING, "Method to test: " . $value);
439 $executor = \&execute_method_test;
440 $$regression_tests[$si][$ri]{'expected-status-code'} = 200;
442 } elsif ($token eq 'blocked url') {
444 l(LL_FILE_LOADING, "URL to block-test: " . $value);
445 $executor = \&execute_block_test;
448 } elsif ($token eq 'url') {
450 l(LL_FILE_LOADING, "Sticky URL to test: " . $value);
451 $type = STICKY_ACTIONS_TEST;
452 $executor = \&execute_sticky_actions_test;
454 } elsif ($token eq 'redirected url') {
456 l(LL_FILE_LOADING, "Redirected URL to test: " . $value);
457 $type = REDIRECT_TEST;
458 $executor = \&execute_redirect_test;
462 die "Incomplete '" . $token . "' support detected.";
465 $$regression_tests[$si][$ri]{'type'} = $type;
466 $$regression_tests[$si][$ri]{'level'} = $type;
467 $$regression_tests[$si][$ri]{'executor'} = $executor;
469 check_for_forbidden_characters($value);
471 $$regression_tests[$si][$ri]{'data'} = $value;
473 # For function that only get passed single tests
474 $$regression_tests[$si][$ri]{'section-id'} = $si;
475 $$regression_tests[$si][$ri]{'regression-test-id'} = $ri;
476 $$regression_tests[$si][$ri]{'number'} = $number - 1;
478 "Regression test " . $number . " (section:" . $si . "):");
481 sub mark_matching_tests_for_skipping($) {
482 my $overwrite_condition = shift;
484 our @regression_tests;
486 for (my $s = 0; $s < @regression_tests; $s++) {
490 while (defined $regression_tests[$s][$r]) {
492 if ($regression_tests[$s][$r]{'data'} eq $overwrite_condition) {
493 my $message = sprintf("Marking test %s for ignoring. Overwrite condition: %s.",
494 $regression_tests[$s][$r]{'number'}, $overwrite_condition);
496 l(LL_FILE_LOADING, $message);
498 # XXX: Should eventually get its own key so get_skip_reason()
499 # can tell about the overwrite condition.
500 $regression_tests[$s][$r]{'ignore'} = 1;
508 # XXX: Shares a lot of code with load_regression_tests_from_file()
509 # that should be factored out.
510 sub load_action_files($) {
514 our @regression_tests;
516 my $actionfiles_ref = shift;
517 my @actionfiles = @{$actionfiles_ref};
519 my $si = 0; # Section index
520 my $ri = -1; # Regression test index
525 for my $file_number (0 .. @actionfiles - 1) {
527 my $curl_url = quote($actionfiles[$file_number]);
528 my $actionfile = undef;
529 my $sticky_actions = undef;
530 my $level_offset = 0;
532 foreach (@{get_cgi_page_or_else($curl_url)}) {
537 if (/<h2>Contents of Actions File (.*?)</) {
541 next unless defined $actionfile;
545 my ($token, $value) = tokenize($_);
547 next unless defined $token;
549 # Load regression tests
550 if ($token eq 'default level offset') {
552 $level_offset = $value;
553 l(LL_FILE_LOADING, "Setting default level offset to " . $level_offset);
556 if (token_starts_new_test($token)) {
558 # Beginning of new regression test.
561 enlist_new_test(\@regression_tests, $token, $value, $si, $ri, $count);
562 $no_checks = 1; # Already validated by enlist_new_test().
563 if ($level_offset != 0) {
564 $regression_tests[$si][$ri]{'level'} += $level_offset;
568 if ($token =~ /level\s+(\d+)/i) {
571 register_dependency($level, $value);
574 if ($token eq 'sticky actions') {
576 # Will be used by each following Sticky URL.
577 $sticky_actions = $value;
578 if ($sticky_actions =~ /{[^}]*\s/) {
579 log_and_die("'Sticky Actions' with whitespace inside the " .
580 "action parameters are currently unsupported.");
584 if ($token eq 'overwrite condition') {
586 l(LL_FILE_LOADING, "Detected overwrite condition: " . $value);
587 # We can only skip matching tests that have already
588 # be loaded but that is exactly what we want anyway.
589 mark_matching_tests_for_skipping($value);
593 if ($si == -1 || $ri == -1) {
594 # No beginning of a test detected yet,
595 # so we don't care about any other test
600 if ($token eq 'expect header') {
602 l(LL_FILE_LOADING, "Detected expectation: " . $value);
603 $regression_tests[$si][$ri]{'expect-header'} = $value;
605 } elsif ($token eq 'tag') {
609 my $tag = parse_tag($value);
611 # We already checked in parse_tag() after filtering
614 l(LL_FILE_LOADING, "Detected TAG: " . $tag);
616 # Save tag for all tests in this section
618 $regression_tests[$si][$ri]{'tag'} = $tag;
624 } elsif ($token eq 'ignore' && $value =~ /Yes/i) {
626 l(LL_FILE_LOADING, "Ignoring section: " . test_content_as_string($regression_tests[$si][$ri]));
627 $regression_tests[$si][$ri]{'ignore'} = 1;
630 } elsif ($token eq 'expect status code') {
632 l(LL_FILE_LOADING, "Expecting status code: " . $value);
633 $regression_tests[$si][$ri]{'expected-status-code'} = $value;
635 } elsif ($token eq 'level') { # XXX: stupid name
637 $value =~ s@(\d+).*@$1@;
638 l(LL_FILE_LOADING, "Level: " . $value);
639 $regression_tests[$si][$ri]{'level'} = $value;
641 } elsif ($token eq 'method') {
643 l(LL_FILE_LOADING, "Method: " . $value);
644 $regression_tests[$si][$ri]{'method'} = $value;
646 } elsif ($token eq 'redirect destination') {
648 l(LL_FILE_LOADING, "Redirect destination: " . $value);
649 $regression_tests[$si][$ri]{'redirect destination'} = $value;
651 } elsif ($token eq 'url') {
653 if (defined $sticky_actions) {
654 die "WTF? Attempted to overwrite Sticky Actions"
655 if defined ($regression_tests[$si][$ri]{'sticky-actions'});
657 l(LL_FILE_LOADING, "Sticky actions: " . $sticky_actions);
658 $regression_tests[$si][$ri]{'sticky-actions'} = $sticky_actions;
660 log_and_die("Sticky URL without Sticky Actions in $actionfile: $value");
665 # We don't use it, so we don't need
667 l(LL_STATUS, "Enabling no_checks for $token") unless $no_checks;
671 unless ($no_checks) {
672 check_for_forbidden_characters($value);
673 check_for_forbidden_characters($token);
678 l(LL_FILE_LOADING, "Done loading " . $count . " regression tests."
679 . " Of which " . $ignored. " will be ignored)\n");
682 ############################################################################
684 # Regression test executing functions
686 ############################################################################
688 # Fisher Yates shuffle from Perl's "How do I shuffle an array randomly?" FAQ
689 sub fisher_yates_shuffle($) {
693 my $j = int rand($i+1);
694 @$deck[$i,$j] = @$deck[$j,$i];
698 sub execute_regression_tests() {
700 our @regression_tests;
701 my $loops = get_cli_option('loops');
703 my $all_failures = 0;
704 my $all_successes = 0;
706 unless (@regression_tests) {
708 l(LL_STATUS, "No regression tests found.");
712 l(LL_STATUS, "Executing regression tests ...");
714 while ($loops-- > 0) {
721 if (cli_option_is_set('shuffle-tests')) {
723 # Shuffle both the test sections and
724 # the tests they contain.
726 # XXX: With the current data layout, shuffling tests
727 # from different sections isn't possible.
728 # Is this worth changing the layout?
729 fisher_yates_shuffle(\@regression_tests);
730 for (my $s = 0; $s < @regression_tests; $s++) {
731 fisher_yates_shuffle($regression_tests[$s]);
735 for (my $s = 0; $s < @regression_tests; $s++) {
739 while (defined $regression_tests[$s][$r]) {
741 unless (cli_option_is_set('shuffle-tests')) {
742 die "Section id mismatch" if ($s != $regression_tests[$s][$r]{'section-id'});
743 die "Regression test id mismatch" if ($r != $regression_tests[$s][$r]{'regression-test-id'});
745 die "Internal error. Test executor missing."
746 unless defined $regression_tests[$s][$r]{executor};
748 my $number = $regression_tests[$s][$r]{'number'};
749 my $skip_reason = get_skip_reason($regression_tests[$s][$r]);
751 if (defined $skip_reason) {
753 my $message = "Skipping test " . $number . ": " . $skip_reason . ".";
754 log_message($message) if (cli_option_is_set('show-skipped-tests'));
759 my $result = $regression_tests[$s][$r]{executor}($regression_tests[$s][$r]);
761 log_result($regression_tests[$s][$r], $result, $tests);
763 $successes += $result;
765 sleep(get_cli_option('sleep-time')) if (cli_option_is_set('sleep-time'));
770 $failures = $tests - $successes;
772 log_message("Executed " . $tests . " regression tests. " .
773 'Skipped ' . $skipped . '. ' .
774 $successes . " successes, " . $failures . " failures.");
776 $all_tests += $tests;
777 $all_failures += $failures;
778 $all_successes += $successes;
781 if (get_cli_option('loops') > 1) {
782 log_message("Total: Executed " . $all_tests . " regression tests. " .
783 $all_successes . " successes, " . $all_failures . " failures.");
787 sub get_skip_reason($) {
789 my $skip_reason = undef;
791 if ($test->{'ignore'}) {
793 $skip_reason = "Ignore flag is set";
795 } elsif (cli_option_is_set('test-number') and
796 get_cli_option('test-number') != $test->{'number'}) {
798 $skip_reason = "Only executing test " . get_cli_option('test-number');
802 $skip_reason = level_is_unacceptable($test->{'level'});
808 sub level_is_unacceptable($) {
810 my $min_level = get_cli_option('min-level');
811 my $max_level = get_cli_option('max-level');
812 my $required_level = cli_option_is_set('level') ?
813 get_cli_option('level') : $level;
816 if ($required_level != $level) {
818 $reason = "Level doesn't match (" . $level .
819 " != " . $required_level . ")"
821 } elsif ($level < $min_level) {
823 $reason = "Level too low (" . $level . " < " . $min_level . ")";
825 } elsif ($level > $max_level) {
827 $reason = "Level too high (" . $level . " > " . $max_level . ")";
831 $reason = dependency_unsatisfied($level);
837 sub dependency_unsatisfied($) {
842 our %privoxy_features;
844 my $dependency_problem = undef;
846 if (defined ($dependencies{$level}{'config line'})) {
848 my $dependency = $dependencies{$level}{'config line'};
849 $dependency_problem = "depends on config line matching: '" . $dependency . "'";
851 foreach (@privoxy_config) {
854 $dependency_problem = undef;
861 if (defined ($dependencies{$level}{'feature status'})
862 and not defined $dependency_problem) {
864 my $dependency = $dependencies{$level}{'feature status'};
865 my ($feature, $status) = $dependency =~ /([^\s]*)\s+(Yes|No)/;
867 unless (defined($privoxy_features{$feature})
868 and ($privoxy_features{$feature} eq $status))
870 $dependency_problem = "depends on '" . $feature .
871 "' being set to '" . $status . "'";
875 return $dependency_problem;
878 sub register_dependency($$) {
881 my $dependency = shift;
884 if ($dependency =~ /config line\s+(.*)/) {
886 $dependencies{$level}{'config line'} = $1;
888 } elsif ($dependency =~ /feature status\s+(.*)/) {
890 $dependencies{$level}{'feature status'} = $1;
894 log_and_die("Didn't recognize dependency: $dependency.");
898 sub execute_method_test($) {
901 our $privoxy_cgi_url;
905 my $method = $test->{'data'};
907 my $curl_parameters = '';
908 my $expected_status_code = $test->{'expected-status-code'};
910 $curl_parameters .= '--request ' . $method . ' ';
911 # Don't complain about the 'missing' body
912 $curl_parameters .= '--head ' if ($method =~ /^HEAD$/i);
914 $curl_parameters .= $privoxy_cgi_url;
916 $buffer_ref = get_page_with_curl($curl_parameters);
917 $status_code = get_status_code($buffer_ref);
919 return check_status_code_result($status_code, $expected_status_code);
922 sub execute_redirect_test($) {
928 my $curl_parameters = '';
929 my $url = $test->{'data'};
930 my $redirect_destination;
931 my $expected_redirect_destination = $test->{'redirect destination'};
933 # XXX: Check if a redirect actually applies before doing the request.
934 # otherwise the test may hit a real server in failure cases.
936 $curl_parameters .= '--head ';
938 $curl_parameters .= quote($url);
940 $buffer_ref = get_page_with_curl($curl_parameters);
941 $status_code = get_status_code($buffer_ref);
943 if ($status_code ne "302") {
944 l(LL_VERBOSE_FAILURE,
945 "Ooops. Expected redirect to: '" . $expected_redirect_destination
946 . "' but got a response with status code: " . $status_code);
949 foreach (@{$buffer_ref}) {
950 if (/^Location: (.*)\r\n/) {
951 $redirect_destination = $1;
956 my $success = ($redirect_destination eq $expected_redirect_destination);
959 l(LL_VERBOSE_FAILURE,
960 "Ooops. Expected redirect to: '" . $expected_redirect_destination
961 . "' but the redirect leads to: '" . $redirect_destination. "'");
967 sub execute_dumb_fetch_test($) {
970 our $privoxy_cgi_url;
975 my $curl_parameters = '';
976 my $expected_status_code = $test->{'expected-status-code'};
978 if (defined $test->{method}) {
979 $curl_parameters .= '--request ' . quote($test->{method}) . ' ';
981 if ($test->{type} == TRUSTED_CGI_REQUEST) {
982 $curl_parameters .= '--referer ' . quote($privoxy_cgi_url) . ' ';
985 $curl_parameters .= quote($test->{'data'});
987 $buffer_ref = get_page_with_curl($curl_parameters);
988 $status_code = get_status_code($buffer_ref);
990 return check_status_code_result($status_code, $expected_status_code);
993 sub execute_block_test($) {
996 my $url = $test->{'data'};
997 my $final_results = get_final_results($url);
999 return defined $final_results->{'+block'};
1002 sub execute_sticky_actions_test($) {
1005 my $url = $test->{'data'};
1006 my $verified_actions = 0;
1007 # XXX: splitting currently doesn't work for actions whose parameters contain spaces.
1008 my @sticky_actions = split(/\s+/, $test->{'sticky-actions'});
1009 my $final_results = get_final_results($url);
1011 foreach my $sticky_action (@sticky_actions) {
1013 if (defined $final_results->{$sticky_action}) {
1015 $verified_actions++;
1017 } elsif ($sticky_action =~ /-.*\{/) {
1019 # Disabled multi actions aren't explicitly listed as
1020 # disabled and thus have to be checked by verifying
1021 # that they aren't enabled.
1022 $verified_actions++;
1025 l(LL_VERBOSE_FAILURE,
1026 "Ooops. '$sticky_action' is not among the final results.");
1030 return $verified_actions == @sticky_actions;
1033 sub get_final_results($) {
1036 our $privoxy_cgi_url;
1038 my $curl_parameters = '';
1039 my %final_results = ();
1040 my $final_results_reached = 0;
1042 die "Unacceptable characters in $url" if $url =~ m@[\\'"]@;
1043 # XXX: should be URL-encoded properly
1050 $curl_parameters .= quote($privoxy_cgi_url . 'show-url-info?url=' . $url);
1052 foreach (@{get_cgi_page_or_else($curl_parameters)}) {
1054 $final_results_reached = 1 if (m@<h2>Final results:</h2>@);
1056 next unless ($final_results_reached);
1059 # Privoxy versions before 3.0.16 add a space
1060 # between action name and parameters, therefore
1062 if (m@<br>([-+])<a.*>([^>]*)</a>(?: ?(\{.*\}))?@) {
1066 if (defined $parameter) {
1067 # In case the caller needs to check
1068 # the action and its parameter
1069 $final_results{$action . $parameter} = 1;
1071 # In case the action doesn't have parameters
1072 # or the caller doesn't care for the parameter.
1073 $final_results{$action} = 1;
1077 return \%final_results;
1080 sub check_status_code_result($$) {
1082 my $status_code = shift;
1083 my $expected_status_code = shift;
1086 unless (defined $status_code) {
1088 # XXX: should probably be caught earlier.
1089 l(LL_VERBOSE_FAILURE,
1090 "Ooops. We expected status code " . $expected_status_code . ", but didn't get any status code at all.");
1092 } elsif ($expected_status_code == $status_code) {
1095 l(LL_VERBOSE_SUCCESS,
1096 "Yay. We expected status code " . $expected_status_code . ", and received: " . $status_code . '.');
1098 } elsif (cli_option_is_set('fuzzer-feeding') and $status_code == 123) {
1100 l(LL_VERBOSE_FAILURE,
1101 "Oh well. Status code lost while fuzzing. Can't check if it was " . $expected_status_code . '.');
1105 l(LL_VERBOSE_FAILURE,
1106 "Ooops. We expected status code " . $expected_status_code . ", but received: " . $status_code . '.');
1112 sub execute_client_header_regression_test($) {
1118 $buffer_ref = get_show_request_with_curl($test);
1120 $header = get_header($buffer_ref, $test);
1122 return check_header_result($test, $header);
1125 sub execute_server_header_regression_test($) {
1131 $buffer_ref = get_head_with_curl($test);
1133 $header = get_server_header($buffer_ref, $test);
1135 return check_header_result($test, $header);
1138 sub interpret_result($) {
1139 my $success = shift;
1140 return $success ? "Success" : "Failure";
1143 sub check_header_result($$) {
1148 my $expect_header = $test->{'expect-header'};
1151 if ($expect_header eq 'NO CHANGE') {
1153 $success = (defined($header) and $header eq $test->{'data'});
1156 $header = "REMOVAL" unless defined $header;
1157 l(LL_VERBOSE_FAILURE,
1158 "Ooops. Got: '" . $header . "' while expecting: '" . $expect_header . "'");
1161 } elsif ($expect_header eq 'REMOVAL') {
1163 # XXX: Use more reliable check here and make sure
1164 # the header has a different name.
1165 $success = not (defined($header) and $header eq $test->{'data'});
1168 l(LL_VERBOSE_FAILURE,
1169 "Ooops. Expected removal but: '" . $header . "' is still there.");
1172 } elsif ($expect_header eq 'SOME CHANGE') {
1174 $success = (defined($header) and $header ne $test->{'data'});
1177 $header = "REMOVAL" unless defined $header;
1178 l(LL_VERBOSE_FAILURE,
1179 "Ooops. Got: '" . $header . "' while expecting: SOME CHANGE");
1184 $success = (defined($header) and $header eq $expect_header);
1187 $header = "No matching header" unless defined $header; # XXX: No header detected to be precise
1188 l(LL_VERBOSE_FAILURE,
1189 "Ooops. Got: '" . $header . "' while expecting: '" . $expect_header . "'");
1195 sub get_header_name($) {
1199 $header =~ s@(.*?: ).*@$1@;
1204 sub get_header($$) {
1206 our $filtered_request = '';
1208 my $buffer_ref = shift;
1211 my @buffer = @{$buffer_ref};
1213 my $expect_header = $test->{'expect-header'};
1215 die "get_header called with no expect header" unless defined $expect_header;
1218 my $processed_request_reached = 0;
1219 my $read_header = 0;
1220 my $processed_request = '';
1224 if ($expect_header eq 'REMOVAL'
1225 or $expect_header eq 'NO CHANGE'
1226 or $expect_header eq 'SOME CHANGE') {
1228 $expect_header = $test->{'data'};
1231 $header_to_get = get_header_name($expect_header);
1235 # Skip everything before the Processed request
1236 if (/Processed Request/) {
1237 $processed_request_reached = 1;
1240 next unless $processed_request_reached;
1242 # End loop after the Processed request
1243 last if (/<\/pre>/);
1245 # Ditch tags and leading/trailing white space.
1249 # Decode characters we care about.
1252 $filtered_request .= "\n" . $_;
1254 if (/^$header_to_get/) {
1264 sub get_server_header($$) {
1266 my $buffer_ref = shift;
1269 my @buffer = @{$buffer_ref};
1271 my $expect_header = $test->{'expect-header'};
1275 # XXX: Should be caught before starting to test.
1276 log_and_die("No expect header for test " . $test->{'number'})
1277 unless defined $expect_header;
1279 if ($expect_header eq 'REMOVAL'
1280 or $expect_header eq 'NO CHANGE'
1281 or $expect_header eq 'SOME CHANGE') {
1283 $expect_header = $test->{'data'};
1286 $header_to_get = get_header_name($expect_header);
1290 # XXX: should probably verify that the request
1291 # was actually answered by Fellatio.
1292 if (/^$header_to_get/) {
1294 $header =~ s@\s*$@@g;
1302 sub get_status_code($) {
1304 my $buffer_ref = shift;
1305 our $privoxy_cgi_url;
1307 my $skip_connection_established_response = $privoxy_cgi_url =~ m@^https://@;
1308 my @buffer = @{$buffer_ref};
1312 if ($skip_connection_established_response) {
1314 next if (m@^HTTP/1\.1 200 Connection established@);
1315 next if (m@^\r\n$@);
1316 $skip_connection_established_response = 0;
1319 if (/^HTTP\/\d\.\d (\d{3})/) {
1325 return '123' if cli_option_is_set('fuzzer-feeding');
1327 log_and_die('Unexpected buffer line: "' . $_ . '"');
1332 sub get_test_keys() {
1333 return ('tag', 'data', 'expect-header', 'ignore');
1337 sub test_content_as_string($) {
1343 foreach my $key (get_test_keys()) {
1344 $test->{$key} = 'Not set' unless (defined $test->{$key});
1347 $s .= 'Tag: ' . $test->{'tag'};
1349 $s .= 'Set header: ' . $test->{'data'}; # XXX: adjust for other test types
1351 $s .= 'Expected header: ' . $test->{'expect-header'};
1353 $s .= 'Ignore: ' . $test->{'ignore'};
1358 sub fuzz_header($) {
1360 my $white_space = int(rand(2)) - 1 ? " " : "\t";
1362 $white_space = $white_space x (1 + int(rand(5)));
1364 # Only fuzz white space before the first quoted token.
1365 # (Privoxy doesn't touch white space inside quoted tokens
1366 # and modifying it would cause the tests to fail).
1367 $header =~ s@(^[^"]*?)\s@$1$white_space@g;
1372 ############################################################################
1374 # HTTP fetch functions
1376 ############################################################################
1378 sub get_cgi_page_or_else($) {
1380 my $cgi_url = shift;
1381 my $content_ref = get_page_with_curl($cgi_url);
1382 my $status_code = get_status_code($content_ref);
1384 if (200 != $status_code) {
1386 my $log_message = "Failed to fetch Privoxy CGI page '$cgi_url'. " .
1387 "Received status code ". $status_code .
1388 " while only 200 is acceptable.";
1390 if (cli_option_is_set('fuzzer-feeding')) {
1392 $log_message .= " Ignored due to fuzzer feeding.";
1393 l(LL_SOFT_ERROR, $log_message)
1397 log_and_die($log_message);
1401 return $content_ref;
1404 # XXX: misleading name
1405 sub get_show_request_with_curl($) {
1407 our $privoxy_cgi_url;
1410 my $curl_parameters = ' ';
1411 my $header = $test->{'data'};
1413 if (cli_option_is_set('header-fuzzing')) {
1414 $header = fuzz_header($header);
1417 # Enable the action to test
1418 $curl_parameters .= '-H \'X-Privoxy-Control: ' . $test->{'tag'} . '\' ';
1420 # Add the header to filter
1421 if ($privoxy_cgi_url =~ m@^https://@ and $header =~ m@^Host:@) {
1422 $curl_parameters .= '--proxy-header \'' . $header . '\' ';
1424 $curl_parameters .= '-H \'' . $header . '\' ';
1427 $curl_parameters .= ' ';
1428 $curl_parameters .= $privoxy_cgi_url;
1429 $curl_parameters .= 'show-request';
1431 return get_cgi_page_or_else($curl_parameters);
1434 sub get_head_with_curl($) {
1436 our $fellatio_url = FELLATIO_URL;
1439 my $curl_parameters = ' ';
1441 # Enable the action to test
1442 $curl_parameters .= '-H \'X-Privoxy-Control: ' . $test->{'tag'} . '\' ';
1443 # The header to filter
1444 $curl_parameters .= '-H \'X-Gimme-Head-With: ' . $test->{'data'} . '\' ';
1445 $curl_parameters .= '--head ';
1447 $curl_parameters .= ' ';
1448 $curl_parameters .= $fellatio_url;
1450 return get_page_with_curl($curl_parameters);
1453 sub get_page_with_curl($) {
1457 my $parameters = shift;
1459 my $curl_line = get_cli_option('curl');
1460 my $retries_left = get_cli_option('retries') + 1;
1463 if (defined $proxy) {
1464 $curl_line .= ' --proxy ' . quote($proxy);
1466 # We want to see the HTTP status code
1467 $curl_line .= " --include ";
1468 # Let Privoxy emit two log messages less.
1469 $curl_line .= ' -H \'Proxy-Connection:\' ' unless $parameters =~ /Proxy-Connection:/;
1470 $curl_line .= ' -H \'Connection: close\' ' unless $parameters =~ /Connection:/;
1471 # We don't care about fetch statistic.
1472 $curl_line .= " -s ";
1473 # We do care about the failure reason if any.
1474 $curl_line .= " -S ";
1475 # We want to advertise ourselves
1476 $curl_line .= " --user-agent '" . PRT_VERSION . "' ";
1477 # We aren't too patient
1478 $curl_line .= " --max-time '" . get_cli_option('max-time') . "' ";
1479 # We don't want curl to treat "[]", "{}" etc. special
1480 $curl_line .= " --globoff ";
1482 $curl_line .= $parameters;
1483 # XXX: still necessary?
1484 $curl_line .= ' 2>&1';
1486 l(LL_PAGE_FETCHING, "Executing: " . $curl_line);
1489 @buffer = `$curl_line`;
1492 log_and_die("Executing '$curl_line' failed.") unless @buffer;
1493 $failure_reason = array_as_string(\@buffer);
1494 chomp $failure_reason;
1495 l(LL_SOFT_ERROR, "Fetch failure: '" . $failure_reason . $! ."'");
1497 } while ($? && --$retries_left);
1499 unless ($retries_left) {
1500 log_and_die("Running curl failed " . get_cli_option('retries') .
1501 " times in a row. Last error: '" . $failure_reason . "'.");
1508 ############################################################################
1512 ############################################################################
1514 sub array_as_string($) {
1515 my $array_ref = shift;
1518 foreach (@{$array_ref}) {
1527 log_message('Test is:' . test_content_as_string($test));
1533 my $this_level = shift;
1534 my $message = shift;
1536 log_message($message) if ($log_level & $this_level);
1539 sub log_and_die($) {
1540 my $message = shift;
1542 log_message('Oh noes. ' . $message . ' Fatal error. Exiting.');
1546 sub log_message($) {
1548 my $message = shift;
1552 our $leading_log_date;
1553 our $leading_log_time;
1555 my $time_stamp = '';
1556 my ( $sec, $min, $hour, $mday, $mon, $year, $wday, $yday, $isdst ) = localtime time;
1558 if ($leading_log_date || $leading_log_time) {
1560 if ($leading_log_date) {
1563 $time_stamp = sprintf("%i-%.2i-%.2i", $year, $mon, $mday);
1566 if ($leading_log_time) {
1567 $time_stamp .= ' ' if $leading_log_date;
1568 $time_stamp.= sprintf("%.2i:%.2i:%.2i", $hour, $min, $sec);
1571 $message = $time_stamp . ": " . $message;
1574 printf("%s\n", $message);
1577 sub log_result($$) {
1579 our $filtered_request;
1585 my $message = sprintf("%s for test %d",
1586 interpret_result($result),
1589 if (cli_option_is_set('verbose')) {
1590 $message .= sprintf(" (%d/%d/%d)", $number,
1591 $test->{'section-id'},
1592 $test->{'regression-test-id'});
1597 if ($test->{'type'} == CLIENT_HEADER_TEST) {
1599 $message .= 'Header ';
1600 $message .= quote($test->{'data'});
1601 $message .= ' and tag ';
1602 $message .= quote($test->{'tag'});
1604 } elsif ($test->{'type'} == SERVER_HEADER_TEST) {
1606 $message .= 'Request Header ';
1607 $message .= quote($test->{'data'});
1608 $message .= ' and tag ';
1609 $message .= quote($test->{'tag'});
1611 } elsif ($test->{'type'} == DUMB_FETCH_TEST) {
1614 $message .= quote($test->{'data'});
1615 $message .= ' and expected status code ';
1616 $message .= quote($test->{'expected-status-code'});
1618 } elsif ($test->{'type'} == TRUSTED_CGI_REQUEST) {
1620 $message .= 'CGI URL ';
1621 $message .= quote($test->{'data'});
1622 $message .= ' and expected status code ';
1623 $message .= quote($test->{'expected-status-code'});
1625 } elsif ($test->{'type'} == METHOD_TEST) {
1627 $message .= 'HTTP method ';
1628 $message .= quote($test->{'data'});
1629 $message .= ' and expected status code ';
1630 $message .= quote($test->{'expected-status-code'});
1632 } elsif ($test->{'type'} == BLOCK_TEST) {
1634 $message .= 'Supposedly-blocked URL: ';
1635 $message .= quote($test->{'data'});
1637 } elsif ($test->{'type'} == STICKY_ACTIONS_TEST) {
1639 $message .= 'Sticky Actions: ';
1640 $message .= quote($test->{'sticky-actions'});
1641 $message .= ' and URL: ';
1642 $message .= quote($test->{'data'});
1644 } elsif ($test->{'type'} == REDIRECT_TEST) {
1646 $message .= 'Redirected URL: ';
1647 $message .= quote($test->{'data'});
1648 $message .= ' and redirect destination: ';
1649 $message .= quote($test->{'redirect destination'});
1653 die "Incomplete support for test type " . $test->{'type'} . " detected.";
1656 log_message($message) if (!$result or cli_option_is_set('verbose'));
1661 return '\'' . $s . '\'';
1664 sub print_version() {
1665 printf PRT_VERSION . "\n";
1668 sub list_test_types() {
1670 'Client header test' => CLIENT_HEADER_TEST,
1671 'Server header test' => 2,
1672 'Dumb fetch test' => 3,
1674 'Sticky action test' => 5,
1675 'Trusted CGI test' => 6,
1677 'Redirect test' => 108,
1680 print "\nThe supported test types and their default levels are:\n";
1681 foreach my $test_type (sort { $test_types{$a} <=> $test_types{$b} } keys %test_types) {
1682 printf " %-20s -> %3.d\n", $test_type, $test_types{$test_type};
1689 our $privoxy_cgi_url;
1695 Options and their default values if they have any:
1697 [--curl $cli_options{'curl'}]
1698 [--debug $cli_options{'debug'}]
1699 [--forks $cli_options{'forks'}]
1706 [--loops $cli_options{'loops'}]
1707 [--max-level $cli_options{'max-level'}]
1708 [--max-time $cli_options{'max-time'}]
1709 [--min-level $cli_options{'min-level'}]
1710 [--privoxy-address $cli_options{'privoxy-address'}]
1711 [--privoxy-cgi-prefix $privoxy_cgi_url]
1712 [--retries $cli_options{'retries'}]
1713 [--show-skipped-tests]
1715 [--sleep-time $cli_options{'sleep-time'}]
1726 Try "perldoc $0" for more information
1733 sub init_cli_options() {
1739 $cli_options{'curl'} = CURL;
1740 $cli_options{'debug'} = $log_level;
1741 $cli_options{'forks'} = CLI_FORKS;
1742 $cli_options{'loops'} = CLI_LOOPS;
1743 $cli_options{'max-level'} = CLI_MAX_LEVEL;
1744 $cli_options{'max-time'} = CLI_MAX_TIME;
1745 $cli_options{'min-level'} = CLI_MIN_LEVEL;
1746 $cli_options{'sleep-time'}= CLI_SLEEP_TIME;
1747 $cli_options{'retries'} = CLI_RETRIES;
1748 $cli_options{'privoxy-address'} = $proxy;
1751 sub parse_cli_options() {
1755 our $privoxy_cgi_url;
1760 'check-bad-ssl' => \$cli_options{'check-bad-ssl'},
1761 'curl=s' => \$cli_options{'curl'},
1762 'debug=i' => \$cli_options{'debug'},
1763 'forks=i' => \$cli_options{'forks'},
1764 'fuzzer-address=s' => \$cli_options{'fuzzer-address'},
1765 'fuzzer-feeding' => \$cli_options{'fuzzer-feeding'},
1766 'header-fuzzing' => \$cli_options{'header-fuzzing'},
1768 'level=i' => \$cli_options{'level'},
1769 'local-test-file=s' => \$cli_options{'local-test-file'},
1770 'loops=i' => \$cli_options{'loops'},
1771 'max-level=i' => \$cli_options{'max-level'},
1772 'max-time=i' => \$cli_options{'max-time'},
1773 'min-level=i' => \$cli_options{'min-level'},
1774 'privoxy-address=s' => \$cli_options{'privoxy-address'},
1775 'privoxy-cgi-prefix=s' => \$privoxy_cgi_url, # XXX: Should use cli_options()
1776 'retries=i' => \$cli_options{'retries'},
1777 'shuffle-tests' => \$cli_options{'shuffle-tests'},
1778 'show-skipped-tests' => \$cli_options{'show-skipped-tests'},
1779 'sleep-time=i' => \$cli_options{'sleep-time'},
1780 'test-number=i' => \$cli_options{'test-number'},
1781 'verbose' => \$cli_options{'verbose'},
1782 'version' => sub {print_version && exit(0)}
1784 $log_level |= $cli_options{'debug'};
1786 if ($cli_options{'min-level'} > $cli_options{'max-level'}) {
1787 log_message("Increasing --max-level to --min-level " . $cli_options{'min-level'});
1788 $cli_options{'max-level'} = $cli_options{'min-level'};
1792 sub cli_option_is_set($) {
1795 my $cli_option = shift;
1797 return defined $cli_options{$cli_option};
1800 sub get_cli_option($) {
1803 my $cli_option = shift;
1805 die "Unknown CLI option: $cli_option" unless defined $cli_options{$cli_option};
1807 return $cli_options{$cli_option};
1810 sub init_proxy_settings($) {
1815 if (($choice eq 'fuzz-proxy') and cli_option_is_set('fuzzer-address')) {
1816 $proxy = get_cli_option('fuzzer-address');
1819 if ((not defined $proxy) or ($choice eq 'vanilla-proxy')) {
1821 if (cli_option_is_set('privoxy-address')) {
1822 $proxy .= get_cli_option('privoxy-address');
1827 sub start_forks($) {
1830 log_and_die("Invalid --fork value: " . $forks . ".") if ($forks < 0);
1832 foreach my $fork (1 .. $forks) {
1833 log_message("Starting fork $fork");
1835 if (defined $pid && !$pid) {
1841 sub check_bad_ssl() {
1843 my @bad_ssl_urls_to_check = (
1844 "https://expired.badssl.com/",
1845 "https://wrong.host.badssl.com/",
1846 "https://self-signed.badssl.com/",
1847 "https://untrusted-root.badssl.com/",
1848 "https://no-common-name.badssl.com/", # XXX: Certificate has expired ...
1849 "https://no-subject.badssl.com/", # XXX: Certificate has expired ...
1850 "https://incomplete-chain.badssl.com/",
1852 # This is needed for get_status_code() to skip the
1853 # status code from the "HTTP/1.1 200 Connection established"
1855 our $privoxy_cgi_url = "https://p.p/";
1857 log_message("Requesting pages from badssl.com with various " .
1858 "certificate problems. This will only work if Privoxy " .
1859 "has been configured properly and can reach the Internet.");
1861 foreach my $url_to_check (@bad_ssl_urls_to_check) {
1862 my ($buffer_ref, $status_code);
1863 log_message("Requesting $url_to_check");
1865 $buffer_ref = get_page_with_curl($url_to_check);
1866 $status_code = get_status_code($buffer_ref);
1868 if (!check_status_code_result($status_code, "403")) {
1873 if ($failures == 0) {
1874 log_message("All requests resulted in status code 403 as expected.");
1876 log_message("There were $failures requests that did not result in status code 403!");
1884 init_our_variables();
1885 parse_cli_options();
1886 init_proxy_settings('vanilla-proxy');
1887 if (cli_option_is_set('check-bad-ssl')) {
1888 exit check_bad_ssl();
1890 load_regression_tests();
1891 init_proxy_settings('fuzz-proxy');
1892 start_forks(get_cli_option('forks')) if cli_option_is_set('forks');
1893 execute_regression_tests();
1900 B<privoxy-regression-test> - A regression test "framework" for Privoxy.
1904 B<privoxy-regression-test> [B<--check-bad-ssl>] [B<--curl curl>] [B<--debug bitmask>]
1905 [B<--forks> forks] [B<--fuzzer-feeding>] [B<--fuzzer-feeding>] [B<--help>] [B<--level level>]
1906 [B<--local-test-file testfile>] [B<--loops count>] [B<--max-level max-level>]
1907 [B<--max-time max-time>] [B<--min-level min-level>] B<--privoxy-address proxy-address>
1908 B<--privoxy-cgi-prefix cgi-prefix> [B<--retries retries>] [B<--test-number test-number>]
1909 [B<--show-skipped-tests>] [B<--sleep-time> seconds] [B<--verbose>]
1914 Privoxy-Regression-Test is supposed to one day become
1915 a regression test suite for Privoxy. It's not quite there
1916 yet, however, and can currently only test header actions,
1917 check the returned status code for requests to arbitrary
1918 URLs and verify which actions are applied to them.
1920 Client header actions are tested by requesting
1921 B<http://p.p/show-request> and checking whether
1922 or not Privoxy modified the original request as expected.
1924 The original request contains both the header the action-to-be-tested
1925 acts upon and an additional tagger-triggering header that enables
1928 Applied actions are checked through B<http://p.p/show-url-info>.
1930 =head1 CONFIGURATION FILE SYNTAX
1932 Privoxy-Regression-Test's configuration is embedded in
1933 Privoxy action files and loaded through Privoxy's web interface.
1935 It makes testing a Privoxy version running on a remote system easier
1936 and should prevent you from updating your tests without updating Privoxy's
1937 configuration accordingly.
1939 A client-header-action test section looks like this:
1941 # Set Header = Referer: http://www.example.org.zwiebelsuppe.exit/
1942 # Expect Header = Referer: http://www.example.org/
1943 {+client-header-filter{hide-tor-exit-notation} -hide-referer}
1944 TAG:^client-header-filter\{hide-tor-exit-notation\}$
1946 The example above causes Privoxy-Regression-Test to set
1947 the header B<Referer: http://www.example.org.zwiebelsuppe.exit/>
1948 and to expect it to be modified to
1949 B<Referer: http://www.example.org/>.
1951 When testing this section, Privoxy-Regression-Test will set the header
1952 B<X-Privoxy-Control: client-header-filter{hide-tor-exit-notation}>
1953 causing the B<privoxy-control> tagger to create the tag
1954 B<client-header-filter{hide-tor-exit-notation}> which will finally
1955 cause Privoxy to enable the action section.
1957 Note that the actions itself are only used by Privoxy,
1958 Privoxy-Regression-Test ignores them and will be happy
1959 as long as the expectations are satisfied.
1961 A fetch test looks like this:
1963 # Fetch Test = http://p.p/user-manual
1964 # Expect Status Code = 302
1966 It tells Privoxy-Regression-Test to request B<http://p.p/user-manual>
1967 and to expect a response with the HTTP status code B<302>. Obviously that's
1968 not a very thorough test and mainly useful to get some code coverage
1969 for Valgrind or to verify that the templates are installed correctly.
1971 If you want to test CGI pages that require a trusted
1972 referer, you can use:
1974 # Trusted CGI Request = http://p.p/edit-actions
1976 It works like ordinary fetch tests, but sets the referer
1977 header to a trusted value.
1979 If no explicit status code expectation is set, B<200> is used.
1981 To verify that a URL is blocked, use:
1983 # Blocked URL = http://www.example.com/blocked
1985 To verify that a specific set of actions is applied to an URL, use:
1987 # Sticky Actions = +block{foo} +handle-as-empty-document -handle-as-image
1988 # URL = http://www.example.org/my-first-url
1990 The sticky actions will be checked for all URLs below it
1991 until the next sticky actions directive.
1993 To verify that requests for a URL get redirected, use:
1995 # Redirected URL = http://www.example.com/redirect-me
1996 # Redirect Destination = http://www.example.org/redirected
1998 To skip a test, add the following line:
2002 The difference between a skipped test and a removed one is that removing
2003 a test affects the numbers of the following tests, while a skipped test
2004 is still loaded and thus keeps the test numbers unchanged.
2006 Sometimes user modifications intentionally conflict with tests in the
2007 default configuration and thus cause test failures. Adding the Ignore
2008 directive to the failing tests works but is inconvenient as the directive
2009 is likely to get lost with the next update.
2011 Overwrite conditions are an alternative and can be added in any action
2012 file as long as the come after the test that is expected to fail.
2013 They cause all previous tests that match the condition to be skipped.
2015 It is recommended to put the overwrite condition below the custom Privoxy
2016 section that causes the expected test failure and before the custom test
2017 that verifies that tests the now expected behaviour. Example:
2019 # The following section is expected to overwrite a section in
2020 # default.action, whose effect is being tested. Thus also disable
2021 # the test that is now expected to fail and add a new one.
2023 {+block{Facebook makes Firefox even more unstable. Do not want.}}
2024 # Overwrite condition = http://apps.facebook.com/onthefarm/track.php?creative=&cat=friendvisit&subcat=weeds&key=a789a971dc687bee4c20c044834fabdd&next=index.php%3Fref%3Dnotif%26visitId%3D898835505
2025 # Blocked URL = http://apps.facebook.com/
2030 All tests have test levels to let the user
2031 control which ones to execute (see I<OPTIONS> below).
2032 Test levels are either set with the B<Level> directive,
2033 or implicitly through the test type.
2035 Redirect tests default to level 108, block tests to level 7,
2036 fetch tests to level 6, "Sticky Actions" tests default to
2037 level 5, tests for trusted CGI requests to level 3 and
2038 client-header-action tests to level 1.
2040 The current redirect test level is above the default
2041 max-level value as failed tests will result in outgoing
2042 connections. Use the B<--max-level> option to run them
2045 The "Default level offset" directive can be used to change
2046 the default level by a given value. This directive affects
2047 all tests located after it until the end of the file or a another
2048 "Default level offset" directive is reached. The purpose of this
2049 directive is to make it more convenient to skip similar tests in
2050 a given file without having to remove or disable the tests completely.
2054 B<--check-bad-ssl> Instead of running the regression tests
2055 as described above, request pages from badssl.com with bad
2056 certificates to verify that Privoxy is detecting the
2057 certificate issues. Only works if Privoxy has been compiled
2058 with FEATURE_HTTPS_INSPECTION, has been configured properly
2059 and can reach the Internet.
2061 B<--curl curl> Use a non-default curl binary.
2063 B<--debug bitmask> Add the bitmask provided as integer
2064 to the debug settings.
2066 B<--forks forks> Number of forks to start before executing
2067 the regression tests. This is mainly useful for stress-testing.
2069 B<--fuzzer-address> Listening address used when executing
2070 the regression tests. Useful to make sure that the requests
2071 to load the regression tests don't fail due to fuzzing.
2073 B<--fuzzer-feeding> Ignore some errors that would otherwise
2074 cause Privoxy-Regression-Test to abort the test because
2075 they shouldn't happen in normal operation. This option is
2076 intended to be used if Privoxy-Regression-Test is only
2077 used to feed a fuzzer in which case there's a high chance
2078 that Privoxy gets an invalid request and returns an error
2081 B<--help> Shows available command line options.
2083 B<--header-fuzzing> Modifies linear white space in
2084 headers in a way that should not affect the test result.
2086 B<--level level> Only execute tests with the specified B<level>.
2088 B<--local-test-file test-file> Do not get the tests
2089 through Privoxy's web interface, but use a single local
2090 file. Not recommended for testing Privoxy, but can be useful
2091 to "misappropriate" Privoxy-Regression-Test to test other
2092 stuff, like webserver configurations.
2094 B<--loop count> Loop through the regression tests B<count> times.
2095 Useful to feed a fuzzer, or when doing stress tests with
2096 several Privoxy-Regression-Test instances running at the same
2099 B<--max-level max-level> Only execute tests with a B<level>
2100 below or equal to the numerical B<max-level>.
2102 B<--max-time max-time> Give Privoxy B<max-time> seconds
2103 to return data. Increasing the default may make sense when
2104 Privoxy is run through Valgrind, decreasing the default may
2105 make sense when Privoxy-Regression-Test is used to feed
2108 B<--min-level min-level> Only execute tests with a B<level>
2109 above or equal to the numerical B<min-level>.
2110 If the B<min-level> is larger than the B<max-level>,
2111 the B<max-level> is set to the B<min-level>.
2113 B<--privoxy-address proxy-address> Privoxy's listening address.
2114 If it's not set, the value of the environment variable http_proxy
2115 will be used unless the variable isn't set in which case
2116 http://127.0.0.1:8118/ will be used. B<proxy-address> has to
2117 be specified in http_proxy syntax.
2119 B<--privoxy-cgi-prefix privoxy-cgi-prefix> The prefix to use when
2120 building URLs that are supposed to reach Privoxy's CGI interface.
2121 If it's not set, B<http://p.p/> is used, which is supposed to work
2122 with the default Privoxy configuration.
2123 If Privoxy has been built with B<FEATURE_HTTPS_INSPECTION> enabled,
2124 and if https inspection is activated with the B<+https-inspection>
2125 action, this option can be used with
2126 B<https://p.p/> provided the system running Privoxy-Regression-Test
2127 has been configured to trust the certificate used by Privoxy.
2128 Note that there are currently two tests in the official
2129 B<regression-tests.action> file that are expected to fail when
2130 using a B<privoxy-cgi-prefix> with B<https://> and aren't automatically
2133 B<--retries retries> Retry B<retries> times.
2135 B<--test-number test-number> Only run the test with the specified
2138 B<--show-skipped-tests> Log skipped tests even if verbose mode is off.
2140 B<--shuffle-tests> Shuffle test sections and their tests before
2141 executing them. When combined with B<--forks>, this can increase
2142 the chances of detecting race conditions. Of course some problems
2143 are easier to detect without this option.
2145 B<--sleep-time seconds> Wait B<seconds> between tests. Useful when
2146 debugging issues with systems that don't log with millisecond precision.
2148 B<--verbose> Log successful tests as well. By default only
2149 the failures are logged.
2151 B<--version> Print version and exit.
2153 The second dash is optional, options can be shortened,
2154 as long as there are no ambiguities.
2156 =head1 PRIVOXY CONFIGURATION
2158 Privoxy-Regression-Test is shipped with B<regression-tests.action>
2159 which aims to test all official client-header modifying actions
2160 and can be used to verify that the templates and the user manual
2161 files are installed correctly.
2163 To use it, it has to be copied in Privoxy's configuration
2164 directory, and afterwards referenced in Privoxy's configuration
2167 actionsfile regression-tests.action
2169 In general, its tests are supposed to work without changing
2170 any other action files, unless you already added lots of
2171 taggers yourself. If you are using taggers that cause problems,
2172 you might have to temporary disable them for Privoxy's CGI pages.
2174 Some of the regression tests rely on Privoxy features that
2175 may be disabled in your configuration. Tests with a level below
2176 7 are supposed to work with all Privoxy configurations (provided
2177 you didn't build with FEATURE_GRACEFUL_TERMINATION).
2179 Tests with level 9 require Privoxy to deliver the User Manual,
2180 tests with level 12 require the CGI editor to be enabled.
2184 Expect the configuration file syntax to change with future releases.
2188 As Privoxy's B<show-request> page only shows client headers,
2189 Privoxy-Regression-Test can't use it to test Privoxy actions
2190 that modify server headers.
2192 As Privoxy-Regression-Test relies on Privoxy's tag feature to
2193 control the actions to test, it currently only works with
2194 Privoxy 3.0.7 or later.
2196 At the moment Privoxy-Regression-Test fetches Privoxy's
2197 configuration page through I<curl>(1), therefore you have to
2198 have I<curl> installed, otherwise you won't be able to run
2199 Privoxy-Regression-Test in a meaningful way.
2207 Fabian Keil <fk@fabiankeil.de>