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-2021 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.3',
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 # a 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 = 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 [--debug $cli_options{'debug'}]
1698 [--forks $cli_options{'forks'}]
1705 [--loops $cli_options{'loops'}]
1706 [--max-level $cli_options{'max-level'}]
1707 [--max-time $cli_options{'max-time'}]
1708 [--min-level $cli_options{'min-level'}]
1709 [--privoxy-address $cli_options{'privoxy-address'}]
1710 [--privoxy-cgi-prefix $privoxy_cgi_url]
1711 [--retries $cli_options{'retries'}]
1712 [--show-skipped-tests]
1714 [--sleep-time $cli_options{'sleep-time'}]
1725 Try "perldoc $0" for more information
1732 sub init_cli_options() {
1738 $cli_options{'debug'} = $log_level;
1739 $cli_options{'forks'} = CLI_FORKS;
1740 $cli_options{'loops'} = CLI_LOOPS;
1741 $cli_options{'max-level'} = CLI_MAX_LEVEL;
1742 $cli_options{'max-time'} = CLI_MAX_TIME;
1743 $cli_options{'min-level'} = CLI_MIN_LEVEL;
1744 $cli_options{'sleep-time'}= CLI_SLEEP_TIME;
1745 $cli_options{'retries'} = CLI_RETRIES;
1746 $cli_options{'privoxy-address'} = $proxy;
1749 sub parse_cli_options() {
1753 our $privoxy_cgi_url;
1758 'check-bad-ssl' => \$cli_options{'check-bad-ssl'},
1759 'debug=i' => \$cli_options{'debug'},
1760 'forks=i' => \$cli_options{'forks'},
1761 'fuzzer-address=s' => \$cli_options{'fuzzer-address'},
1762 'fuzzer-feeding' => \$cli_options{'fuzzer-feeding'},
1763 'header-fuzzing' => \$cli_options{'header-fuzzing'},
1765 'level=i' => \$cli_options{'level'},
1766 'local-test-file=s' => \$cli_options{'local-test-file'},
1767 'loops=i' => \$cli_options{'loops'},
1768 'max-level=i' => \$cli_options{'max-level'},
1769 'max-time=i' => \$cli_options{'max-time'},
1770 'min-level=i' => \$cli_options{'min-level'},
1771 'privoxy-address=s' => \$cli_options{'privoxy-address'},
1772 'privoxy-cgi-prefix=s' => \$privoxy_cgi_url, # XXX: Should use cli_options()
1773 'retries=i' => \$cli_options{'retries'},
1774 'shuffle-tests' => \$cli_options{'shuffle-tests'},
1775 'show-skipped-tests' => \$cli_options{'show-skipped-tests'},
1776 'sleep-time=i' => \$cli_options{'sleep-time'},
1777 'test-number=i' => \$cli_options{'test-number'},
1778 'verbose' => \$cli_options{'verbose'},
1779 'version' => sub {print_version && exit(0)}
1781 $log_level |= $cli_options{'debug'};
1784 sub cli_option_is_set($) {
1787 my $cli_option = shift;
1789 return defined $cli_options{$cli_option};
1792 sub get_cli_option($) {
1795 my $cli_option = shift;
1797 die "Unknown CLI option: $cli_option" unless defined $cli_options{$cli_option};
1799 return $cli_options{$cli_option};
1802 sub init_proxy_settings($) {
1807 if (($choice eq 'fuzz-proxy') and cli_option_is_set('fuzzer-address')) {
1808 $proxy = get_cli_option('fuzzer-address');
1811 if ((not defined $proxy) or ($choice eq 'vanilla-proxy')) {
1813 if (cli_option_is_set('privoxy-address')) {
1814 $proxy .= get_cli_option('privoxy-address');
1819 sub start_forks($) {
1822 log_and_die("Invalid --fork value: " . $forks . ".") if ($forks < 0);
1824 foreach my $fork (1 .. $forks) {
1825 log_message("Starting fork $fork");
1827 if (defined $pid && !$pid) {
1833 sub check_bad_ssl() {
1835 my @bad_ssl_urls_to_check = (
1836 "https://expired.badssl.com/",
1837 "https://wrong.host.badssl.com/",
1838 "https://self-signed.badssl.com/",
1839 "https://untrusted-root.badssl.com/",
1840 "https://no-common-name.badssl.com/", # XXX: Certificate has expired ...
1841 "https://no-subject.badssl.com/", # XXX: Certificate has expired ...
1842 "https://incomplete-chain.badssl.com/",
1844 # This is needed for get_status_code() to skip the
1845 # status code from the "HTTP/1.1 200 Connection established"
1847 our $privoxy_cgi_url = "https://p.p/";
1849 log_message("Requesting pages from badssl.com with various " .
1850 "certificate problems. This will only work if Privoxy " .
1851 "has been configured properly and can reach the Internet.");
1853 foreach my $url_to_check (@bad_ssl_urls_to_check) {
1854 my ($buffer_ref, $status_code);
1855 log_message("Requesting $url_to_check");
1857 $buffer_ref = get_page_with_curl($url_to_check);
1858 $status_code = get_status_code($buffer_ref);
1860 if (!check_status_code_result($status_code, "403")) {
1865 if ($failures == 0) {
1866 log_message("All requests resulted in status code 403 as expected.");
1868 log_message("There were $failures requests that did not result in status code 403!");
1876 init_our_variables();
1877 parse_cli_options();
1878 init_proxy_settings('vanilla-proxy');
1879 if (cli_option_is_set('check-bad-ssl')) {
1880 exit check_bad_ssl();
1882 load_regression_tests();
1883 init_proxy_settings('fuzz-proxy');
1884 start_forks(get_cli_option('forks')) if cli_option_is_set('forks');
1885 execute_regression_tests();
1892 B<privoxy-regression-test> - A regression test "framework" for Privoxy.
1896 B<privoxy-regression-test> [B<--check-bad-ssl>] [B<--debug bitmask>] [B<--forks> forks]
1897 [B<--fuzzer-feeding>] [B<--fuzzer-feeding>] [B<--help>] [B<--level level>]
1898 [B<--local-test-file testfile>] [B<--loops count>] [B<--max-level max-level>]
1899 [B<--max-time max-time>] [B<--min-level min-level>] B<--privoxy-address proxy-address>
1900 B<--privoxy-cgi-prefix cgi-prefix> [B<--retries retries>] [B<--test-number test-number>]
1901 [B<--show-skipped-tests>] [B<--sleep-time> seconds] [B<--verbose>]
1906 Privoxy-Regression-Test is supposed to one day become
1907 a regression test suite for Privoxy. It's not quite there
1908 yet, however, and can currently only test header actions,
1909 check the returned status code for requests to arbitrary
1910 URLs and verify which actions are applied to them.
1912 Client header actions are tested by requesting
1913 B<http://p.p/show-request> and checking whether
1914 or not Privoxy modified the original request as expected.
1916 The original request contains both the header the action-to-be-tested
1917 acts upon and an additional tagger-triggering header that enables
1920 Applied actions are checked through B<http://p.p/show-url-info>.
1922 =head1 CONFIGURATION FILE SYNTAX
1924 Privoxy-Regression-Test's configuration is embedded in
1925 Privoxy action files and loaded through Privoxy's web interface.
1927 It makes testing a Privoxy version running on a remote system easier
1928 and should prevent you from updating your tests without updating Privoxy's
1929 configuration accordingly.
1931 A client-header-action test section looks like this:
1933 # Set Header = Referer: http://www.example.org.zwiebelsuppe.exit/
1934 # Expect Header = Referer: http://www.example.org/
1935 {+client-header-filter{hide-tor-exit-notation} -hide-referer}
1936 TAG:^client-header-filter\{hide-tor-exit-notation\}$
1938 The example above causes Privoxy-Regression-Test to set
1939 the header B<Referer: http://www.example.org.zwiebelsuppe.exit/>
1940 and to expect it to be modified to
1941 B<Referer: http://www.example.org/>.
1943 When testing this section, Privoxy-Regression-Test will set the header
1944 B<X-Privoxy-Control: client-header-filter{hide-tor-exit-notation}>
1945 causing the B<privoxy-control> tagger to create the tag
1946 B<client-header-filter{hide-tor-exit-notation}> which will finally
1947 cause Privoxy to enable the action section.
1949 Note that the actions itself are only used by Privoxy,
1950 Privoxy-Regression-Test ignores them and will be happy
1951 as long as the expectations are satisfied.
1953 A fetch test looks like this:
1955 # Fetch Test = http://p.p/user-manual
1956 # Expect Status Code = 302
1958 It tells Privoxy-Regression-Test to request B<http://p.p/user-manual>
1959 and to expect a response with the HTTP status code B<302>. Obviously that's
1960 not a very thorough test and mainly useful to get some code coverage
1961 for Valgrind or to verify that the templates are installed correctly.
1963 If you want to test CGI pages that require a trusted
1964 referer, you can use:
1966 # Trusted CGI Request = http://p.p/edit-actions
1968 It works like ordinary fetch tests, but sets the referer
1969 header to a trusted value.
1971 If no explicit status code expectation is set, B<200> is used.
1973 To verify that a URL is blocked, use:
1975 # Blocked URL = http://www.example.com/blocked
1977 To verify that a specific set of actions is applied to an URL, use:
1979 # Sticky Actions = +block{foo} +handle-as-empty-document -handle-as-image
1980 # URL = http://www.example.org/my-first-url
1982 The sticky actions will be checked for all URLs below it
1983 until the next sticky actions directive.
1985 To verify that requests for a URL get redirected, use:
1987 # Redirected URL = http://www.example.com/redirect-me
1988 # Redirect Destination = http://www.example.org/redirected
1990 To skip a test, add the following line:
1994 The difference between a skipped test and a removed one is that removing
1995 a test affects the numbers of the following tests, while a skipped test
1996 is still loaded and thus keeps the test numbers unchanged.
1998 Sometimes user modifications intentionally conflict with tests in the
1999 default configuration and thus cause test failures. Adding the Ignore
2000 directive to the failing tests works but is inconvenient as the directive
2001 is likely to get lost with the next update.
2003 Overwrite conditions are an alternative and can be added in any action
2004 file as long as the come after the test that is expected to fail.
2005 They cause all previous tests that match the condition to be skipped.
2007 It is recommended to put the overwrite condition below the custom Privoxy
2008 section that causes the expected test failure and before the custom test
2009 that verifies that tests the now expected behaviour. Example:
2011 # The following section is expected to overwrite a section in
2012 # default.action, whose effect is being tested. Thus also disable
2013 # the test that is now expected to fail and add a new one.
2015 {+block{Facebook makes Firefox even more unstable. Do not want.}}
2016 # Overwrite condition = http://apps.facebook.com/onthefarm/track.php?creative=&cat=friendvisit&subcat=weeds&key=a789a971dc687bee4c20c044834fabdd&next=index.php%3Fref%3Dnotif%26visitId%3D898835505
2017 # Blocked URL = http://apps.facebook.com/
2022 All tests have test levels to let the user
2023 control which ones to execute (see I<OPTIONS> below).
2024 Test levels are either set with the B<Level> directive,
2025 or implicitly through the test type.
2027 Redirect tests default to level 108, block tests to level 7,
2028 fetch tests to level 6, "Sticky Actions" tests default to
2029 level 5, tests for trusted CGI requests to level 3 and
2030 client-header-action tests to level 1.
2032 The current redirect test level is above the default
2033 max-level value as failed tests will result in outgoing
2034 connections. Use the B<--max-level> option to run them
2037 The "Default level offset" directive can be used to change
2038 the default level by a given value. This directive affects
2039 all tests located after it until the end of the file or a another
2040 "Default level offset" directive is reached. The purpose of this
2041 directive is to make it more convenient to skip similar tests in
2042 a given file without having to remove or disable the tests completely.
2046 B<--check-bad-ssl> Instead of running the regression tests
2047 as described above, request pages from badssl.com with bad
2048 certificates to verify that Privoxy is detecting the
2049 certificate issues. Only works if Privoxy has been compiled
2050 with FEATURE_HTTPS_INSPECTION, has been configured properly
2051 and can reach the Internet.
2053 B<--debug bitmask> Add the bitmask provided as integer
2054 to the debug settings.
2056 B<--forks forks> Number of forks to start before executing
2057 the regression tests. This is mainly useful for stress-testing.
2059 B<--fuzzer-address> Listening address used when executing
2060 the regression tests. Useful to make sure that the requests
2061 to load the regression tests don't fail due to fuzzing.
2063 B<--fuzzer-feeding> Ignore some errors that would otherwise
2064 cause Privoxy-Regression-Test to abort the test because
2065 they shouldn't happen in normal operation. This option is
2066 intended to be used if Privoxy-Regression-Test is only
2067 used to feed a fuzzer in which case there's a high chance
2068 that Privoxy gets an invalid request and returns an error
2071 B<--help> Shows available command line options.
2073 B<--header-fuzzing> Modifies linear white space in
2074 headers in a way that should not affect the test result.
2076 B<--level level> Only execute tests with the specified B<level>.
2078 B<--local-test-file test-file> Do not get the tests
2079 through Privoxy's web interface, but use a single local
2080 file. Not recommended for testing Privoxy, but can be useful
2081 to "misappropriate" Privoxy-Regression-Test to test other
2082 stuff, like webserver configurations.
2084 B<--loop count> Loop through the regression tests B<count> times.
2085 Useful to feed a fuzzer, or when doing stress tests with
2086 several Privoxy-Regression-Test instances running at the same
2089 B<--max-level max-level> Only execute tests with a B<level>
2090 below or equal to the numerical B<max-level>.
2092 B<--max-time max-time> Give Privoxy B<max-time> seconds
2093 to return data. Increasing the default may make sense when
2094 Privoxy is run through Valgrind, decreasing the default may
2095 make sense when Privoxy-Regression-Test is used to feed
2098 B<--min-level min-level> Only execute tests with a B<level>
2099 above or equal to the numerical B<min-level>.
2101 B<--privoxy-address proxy-address> Privoxy's listening address.
2102 If it's not set, the value of the environment variable http_proxy
2103 will be used unless the variable isn't set in which case
2104 http://127.0.0.1:8118/ will be used. B<proxy-address> has to
2105 be specified in http_proxy syntax.
2107 B<--privoxy-cgi-prefix privoxy-cgi-prefix> The prefix to use when
2108 building URLs that are supposed to reach Privoxy's CGI interface.
2109 If it's not set, B<http://p.p/> is used, which is supposed to work
2110 with the default Privoxy configuration.
2111 If Privoxy has been built with B<FEATURE_HTTPS_INSPECTION> enabled,
2112 and if https inspection is activated with the B<+https-inspection>
2113 action, this option can be used with
2114 B<https://p.p/> provided the system running Privoxy-Regression-Test
2115 has been configured to trust the certificate used by Privoxy.
2116 Note that there are currently two tests in the official
2117 B<regression-tests.action> file that are expected to fail when
2118 using a B<privoxy-cgi-prefix> with B<https://> and aren't automatically
2121 B<--retries retries> Retry B<retries> times.
2123 B<--test-number test-number> Only run the test with the specified
2126 B<--show-skipped-tests> Log skipped tests even if verbose mode is off.
2128 B<--shuffle-tests> Shuffle test sections and their tests before
2129 executing them. When combined with B<--forks>, this can increase
2130 the chances of detecting race conditions. Of course some problems
2131 are easier to detect without this option.
2133 B<--sleep-time seconds> Wait B<seconds> between tests. Useful when
2134 debugging issues with systems that don't log with millisecond precision.
2136 B<--verbose> Log successful tests as well. By default only
2137 the failures are logged.
2139 B<--version> Print version and exit.
2141 The second dash is optional, options can be shortened,
2142 as long as there are no ambiguities.
2144 =head1 PRIVOXY CONFIGURATION
2146 Privoxy-Regression-Test is shipped with B<regression-tests.action>
2147 which aims to test all official client-header modifying actions
2148 and can be used to verify that the templates and the user manual
2149 files are installed correctly.
2151 To use it, it has to be copied in Privoxy's configuration
2152 directory, and afterwards referenced in Privoxy's configuration
2155 actionsfile regression-tests.action
2157 In general, its tests are supposed to work without changing
2158 any other action files, unless you already added lots of
2159 taggers yourself. If you are using taggers that cause problems,
2160 you might have to temporary disable them for Privoxy's CGI pages.
2162 Some of the regression tests rely on Privoxy features that
2163 may be disabled in your configuration. Tests with a level below
2164 7 are supposed to work with all Privoxy configurations (provided
2165 you didn't build with FEATURE_GRACEFUL_TERMINATION).
2167 Tests with level 9 require Privoxy to deliver the User Manual,
2168 tests with level 12 require the CGI editor to be enabled.
2172 Expect the configuration file syntax to change with future releases.
2176 As Privoxy's B<show-request> page only shows client headers,
2177 Privoxy-Regression-Test can't use it to test Privoxy actions
2178 that modify server headers.
2180 As Privoxy-Regression-Test relies on Privoxy's tag feature to
2181 control the actions to test, it currently only works with
2182 Privoxy 3.0.7 or later.
2184 At the moment Privoxy-Regression-Test fetches Privoxy's
2185 configuration page through I<curl>(1), therefore you have to
2186 have I<curl> installed, otherwise you won't be able to run
2187 Privoxy-Regression-Test in a meaningful way.
2195 Fabian Keil <fk@fabiankeil.de>