- Normalize whitespace in directives.
[privoxy.git] / tools / privoxy-regression-test.pl
1 #!/usr/bin/perl
2
3 ############################################################################
4 #
5 # Privoxy-Regression-Test
6 #
7 # A regression test "framework" for Privoxy. For documentation see:
8 # perldoc privoxy-regression-test.pl
9 #
10 # $Id: privoxy-regression-test.pl,v 1.119 2008/03/21 12:56:20 fk Exp $
11 #
12 # Wish list:
13 #
14 # - Update documentation
15 # - Validate HTTP times.
16 # - Understand default.action.master comment syntax
17 #   and verify that we actually block and unblock what
18 #   the comments claim we do.
19 # - Implement a HTTP_VERSION directive or allow to
20 #   specify whole request lines.
21 # - Support filter regression tests.
22 # - Add option to fork regression tests and run them in parallel,
23 #   possibly optional forever.
24 # - Document magic Expect Header values
25 # - Internal fuzz support?
26 #
27 # Copyright (c) 2007-2008 Fabian Keil <fk@fabiankeil.de>
28 #
29 # Permission to use, copy, modify, and distribute this software for any
30 # purpose with or without fee is hereby granted, provided that the above
31 # copyright notice and this permission notice appear in all copies.
32 #
33 # THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
34 # WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
35 # MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
36 # ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
37 # WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
38 # ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
39 # OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
40 #
41 ############################################################################
42
43 use warnings;
44 use strict;
45 use Getopt::Long;
46
47 use constant {
48                PRT_VERSION => 'Privoxy-Regression-Test 0.2',
49  
50                CURL => 'curl',
51
52                # CLI option defaults
53                CLI_RETRIES  => 1,
54                CLI_LOOPS    => 1,
55                CLI_MAX_TIME => 5,
56                CLI_MIN_LEVEL => 0,
57                CLI_MAX_LEVEL => 25,
58
59                PRIVOXY_CGI_URL => 'http://p.p/',
60                FELLATIO_URL    => 'http://10.0.0.1:8080/',
61                LEADING_LOG_DATE => 1,
62                LEADING_LOG_TIME => 1,
63
64                DEBUG_LEVEL_FILE_LOADING    => 0,
65                DEBUG_LEVEL_PAGE_FETCHING   => 0,
66
67                VERBOSE_TEST_DESCRIPTION    => 1,
68
69                DEBUG_LEVEL_VERBOSE_FAILURE => 1,
70                # XXX: Only partly implemented and mostly useless.
71                DEBUG_LEVEL_VERBOSE_SUCCESS => 0,
72                DEBUG_LEVEL_STATUS          => 1,
73
74                # Internal use, don't modify
75                # Available debug bits:
76                LL_ERROR                   =>  1,
77                LL_VERBOSE_FAILURE         =>  2,
78                LL_PAGE_FETCHING           =>  4,
79                LL_FILE_LOADING            =>  8,
80                LL_VERBOSE_SUCCESS         => 16,
81                LL_STATUS                  => 32,
82                LL_SOFT_ERROR              => 64,
83
84                CLIENT_HEADER_TEST         =>  1,
85                SERVER_HEADER_TEST         =>  2,
86                DUMB_FETCH_TEST            =>  3,
87                METHOD_TEST                =>  4,
88                TRUSTED_CGI_REQUEST        =>  6,
89                BLOCK_TEST                 =>  7,
90 };
91
92 sub init_our_variables () {
93
94     our $leading_log_time = LEADING_LOG_TIME;
95     our $leading_log_date = LEADING_LOG_DATE;
96
97     our $privoxy_cgi_url  = PRIVOXY_CGI_URL;
98
99     our $verbose_test_description = VERBOSE_TEST_DESCRIPTION;
100
101     our $log_level = get_default_log_level();
102
103 }
104
105 sub get_default_log_level () {
106     
107     my $log_level = 0;
108
109     $log_level |= LL_FILE_LOADING    if DEBUG_LEVEL_FILE_LOADING;
110     $log_level |= LL_PAGE_FETCHING   if DEBUG_LEVEL_PAGE_FETCHING;
111     $log_level |= LL_VERBOSE_FAILURE if DEBUG_LEVEL_VERBOSE_FAILURE;
112     $log_level |= LL_VERBOSE_SUCCESS if DEBUG_LEVEL_VERBOSE_SUCCESS;
113     $log_level |= LL_STATUS          if DEBUG_LEVEL_STATUS;
114
115     # These are intended to be always on.
116     $log_level |= LL_SOFT_ERROR;
117     $log_level |= LL_ERROR;
118
119     return $log_level;
120 }
121
122 ############################################################################
123 #
124 # File loading functions
125 #
126 ############################################################################
127
128 sub parse_tag ($) {
129
130     my $tag = shift;
131
132     # Remove anchors
133     $tag =~ s@[\$\^]@@g;
134     # Unescape brackets and dots
135     $tag =~ s@\\(?=[{}().+])@@g;
136
137     # log_message("Parsed tag: " . $tag);
138
139     check_for_forbidden_characters($tag);
140
141     return $tag;
142 }
143
144 sub check_for_forbidden_characters ($) {
145
146     my $tag = shift; # XXX: also used to check values though.
147     my $allowed = '[-=\dA-Za-z~{}:.\/();\s,+@"_%\?&]';
148
149     unless ($tag =~ m/^$allowed*$/) {
150         my $forbidden = $tag;
151         $forbidden =~ s@^$allowed*(.).*@$1@;
152
153         l(LL_ERROR, "'" . $tag . "' contains character '" . $forbidden. "' which is unacceptable.");
154     }
155 }
156
157 sub load_regressions_tests () {
158
159     our $privoxy_cgi_url;
160     our @privoxy_config;
161     my @actionfiles;
162     my $curl_url = '';
163     my $file_number = 0;
164
165     $curl_url .= $privoxy_cgi_url;
166     $curl_url .= 'show-status';
167
168     l(LL_STATUS, "Asking Privoxy for the number of action files available ...");
169
170     foreach (@{get_cgi_page_or_else($curl_url)}) {
171
172         chomp;
173         if (/<td>(.*?)<\/td><td class=\"buttons\"><a href=\"\/show-status\?file=actions&amp;index=(\d+)\">/) {
174
175             my $url = $privoxy_cgi_url . 'show-status?file=actions&index=' . $2;
176             $actionfiles[$file_number++] = $url;
177
178         } elsif (m@config\.html#.*\">([^<]*)</a>\s+(.*)<br>@) {
179
180             my $directive = $1 . " " . $2;
181             push (@privoxy_config, $directive);
182         }
183     }
184
185     l(LL_FILE_LOADING, "Recognized " . @actionfiles . " actions files");
186
187     load_action_files(\@actionfiles);
188 }
189
190 sub token_starts_new_test ($) {
191
192     my $token = shift;
193     my @new_test_directives =
194         ('set header', 'fetch test', 'trusted cgi request', 'request header', 'method test', 'blocked url');
195
196     foreach my $new_test_directive (@new_test_directives) {
197         return 1 if $new_test_directive eq $token;
198     }
199     return 0;
200
201 }
202
203 sub tokenize ($) {
204
205     my ($token, $value) = (undef, undef);
206
207     # Remove leading and trailing white space.
208     s@^\s*@@;
209     s@\s*$@@;
210
211     # Reverse HTML-encoding
212     # XXX: Seriously imcomplete. 
213     s@&quot;@"@g;
214     s@&amp;@&@g;
215
216     # Tokenize
217     if (/^\#\s*([^=:]*?)\s*[=]\s*(.+?)\s*$/) {
218
219         $token = $1;
220         $value = $2;
221
222         $token =~ s@\s\s+@ @g;
223         $token =~ tr/[A-Z]/[a-z]/;
224
225     } elsif (/^TAG\s*:(.*)$/) {
226
227         $token = 'tag';
228         $value = $1;
229
230     }
231
232     return ($token, $value);
233 }
234
235 sub enlist_new_test ($$$$$$) {
236
237     my ($regression_tests, $token, $value, $si, $ri, $number) = @_;
238
239     if ($token eq 'set header') {
240
241         l(LL_FILE_LOADING, "Header to set: " . $value);
242         ${$regression_tests}[$si][$ri]{'type'} = CLIENT_HEADER_TEST;
243         # Implicit default
244         $$regression_tests[$si][$ri]{'level'} = CLIENT_HEADER_TEST;
245
246     } elsif ($token eq 'request header') {
247
248         l(LL_FILE_LOADING, "Header to request: " . $value);
249         $$regression_tests[$si][$ri]{'type'} = SERVER_HEADER_TEST;
250         # Implicit default
251         $$regression_tests[$si][$ri]{'expected-status-code'} = 200;
252         $$regression_tests[$si][$ri]{'level'} = SERVER_HEADER_TEST;
253
254     } elsif ($token eq 'trusted cgi request') {
255
256         l(LL_FILE_LOADING, "CGI URL to test in a dumb way: " . $value);
257         $$regression_tests[$si][$ri]{'type'} = TRUSTED_CGI_REQUEST;
258         # Implicit default
259         $$regression_tests[$si][$ri]{'expected-status-code'} = 200;
260         $$regression_tests[$si][$ri]{'level'} = TRUSTED_CGI_REQUEST;
261
262     } elsif ($token eq 'fetch test') {
263
264         l(LL_FILE_LOADING, "URL to test in a dumb way: " . $value);
265         $$regression_tests[$si][$ri]{'type'} = DUMB_FETCH_TEST;
266         # Implicit default
267         $$regression_tests[$si][$ri]{'expected-status-code'} = 200;
268         $$regression_tests[$si][$ri]{'level'} = DUMB_FETCH_TEST;
269
270     } elsif ($token eq 'method test') {
271
272         l(LL_FILE_LOADING, "Method to test: " . $value);
273         $$regression_tests[$si][$ri]{'type'} = METHOD_TEST;
274         # Implicit default
275         $$regression_tests[$si][$ri]{'expected-status-code'} = 200;
276         $$regression_tests[$si][$ri]{'level'} = METHOD_TEST;
277
278     } elsif ($token eq 'blocked url') {
279
280         l(LL_FILE_LOADING, "URL to block-test: " . $value);
281         $$regression_tests[$si][$ri]{'type'} = BLOCK_TEST;
282         # Implicit default
283         $$regression_tests[$si][$ri]{'expected-status-code'} = 403;
284         $$regression_tests[$si][$ri]{'level'} = BLOCK_TEST;
285
286     } else {
287
288         die "Incomplete '" . $token . "' support detected."; 
289
290     }
291
292     check_for_forbidden_characters($value);
293
294     $$regression_tests[$si][$ri]{'data'} = $value;
295
296     # For function that only get passed single tests
297     $$regression_tests[$si][$ri]{'section-id'} = $si;
298     $$regression_tests[$si][$ri]{'regression-test-id'} = $ri;
299     $$regression_tests[$si][$ri]{'number'} = $number - 1;
300     l(LL_FILE_LOADING,
301       "Regression test " . $number . " (section:" . $si . "):");
302 }
303
304 sub load_action_files ($) {
305
306     # initialized here
307     our %actions;
308     our @regression_tests;
309
310     my $actionfiles_ref = shift;
311     my @actionfiles = @{$actionfiles_ref};
312
313     my $si = 0;  # Section index
314     my $ri = -1; # Regression test index
315     my $count = 0;
316
317     my $ignored = 0;
318
319     l(LL_STATUS, "Loading regression tests from action file(s) delivered by Privoxy.");
320
321     for my $file_number (0 .. @actionfiles - 1) {
322
323         my $curl_url = ' "' . $actionfiles[$file_number] . '"';
324         my $actionfile = undef;
325
326         foreach (@{get_cgi_page_or_else($curl_url)}) {
327
328             my $no_checks = 0;
329             chomp;
330             
331             if (/<h2>Contents of Actions File (.*?)</) {
332                 $actionfile = $1;
333                 next;
334             }
335             next unless defined $actionfile;
336
337             last if (/<\/pre>/);
338
339             my ($token, $value) = tokenize($_);
340
341             next unless defined $token;
342
343             # Load regression tests
344
345             if (token_starts_new_test($token)) {
346
347                 # Beginning of new regression test.
348                 $ri++;
349                 $count++;
350                 enlist_new_test(\@regression_tests, $token, $value, $si, $ri, $count);
351             }
352
353             if ($token =~ /level\s+(\d+)/i) {
354
355                 my $level = $1;
356                 register_dependency($level, $value);
357             }
358             
359             if ($si == -1 || $ri == -1) {
360                 # No beginning of a test detected yet,
361                 # so we don't care about any other test
362                 # attributes.
363                 next;
364             }
365
366             if ($token eq 'expect header') {
367
368                 l(LL_FILE_LOADING, "Detected expectation: " . $value);
369                 $regression_tests[$si][$ri]{'expect-header'} = $value;
370
371             } elsif ($token eq 'tag') {
372                 
373                 next if ($ri == -1);
374
375                 my $tag = parse_tag($value);
376
377                 # We already checked in parse_tag() after filtering
378                 $no_checks = 1;
379
380                 l(LL_FILE_LOADING, "Detected TAG: " . $tag);
381
382                 # Save tag for all tests in this section
383                 do {
384                     $regression_tests[$si][$ri]{'tag'} = $tag; 
385                 } while ($ri-- > 0);
386
387                 $si++;
388                 $ri = -1;
389
390             } elsif ($token eq 'ignore' && $value =~ /Yes/i) {
391
392                 l(LL_FILE_LOADING, "Ignoring section: " . test_content_as_string($regression_tests[$si][$ri]));
393                 $regression_tests[$si][$ri]{'ignore'} = 1;
394                 $ignored++;
395
396             } elsif ($token eq 'expect status code') {
397
398                 l(LL_FILE_LOADING, "Expecting status code: " . $value);
399                 $regression_tests[$si][$ri]{'expected-status-code'} = $value;
400
401             } elsif ($token eq 'level') { # XXX: stupid name
402
403                 $value =~ s@(\d+).*@$1@;
404                 l(LL_FILE_LOADING, "Level: " . $value);
405                 $regression_tests[$si][$ri]{'level'} = $value;
406
407             } elsif ($token eq 'method') {
408
409                 l(LL_FILE_LOADING, "Method: " . $value);
410                 $regression_tests[$si][$ri]{'method'} = $value;
411
412             } else {
413
414                 # We don't use it, so we don't need
415                 $no_checks = 1;
416             }
417             # XXX: Neccessary?
418             check_for_forbidden_characters($value) unless $no_checks;
419             check_for_forbidden_characters($token);
420         }
421     }
422
423     l(LL_FILE_LOADING, "Done loading " . $count . " regression tests." 
424       . " Of which " . $ignored. " will be ignored)\n");
425 }
426
427 ############################################################################
428 #
429 # Regression test executing functions
430 #
431 ############################################################################
432
433 sub execute_regression_tests () {
434
435     our @regression_tests;
436     my $loops = get_cli_option('loops');
437     my $all_tests    = 0;
438     my $all_failures = 0;
439     my $all_successes = 0;
440
441     unless (@regression_tests) {
442
443         l(LL_STATUS, "No regression tests found.");
444         return;
445     }
446
447     l(LL_STATUS, "Executing regression tests ...");
448
449     while ($loops-- > 0) {
450
451         my $successes = 0;
452         my $tests = 0;
453         my $failures;
454         my $skipped = 0;
455
456         for my $s (0 .. @regression_tests - 1) {
457
458             my $r = 0;
459
460             while (defined $regression_tests[$s][$r]) {
461
462                 die "Section id mismatch" if ($s != $regression_tests[$s][$r]{'section-id'});
463                 die "Regression test id mismatch" if ($r != $regression_tests[$s][$r]{'regression-test-id'});
464
465                 my $number = $regression_tests[$s][$r]{'number'};
466
467                 if ($regression_tests[$s][$r]{'ignore'}
468                     or level_is_unacceptable($regression_tests[$s][$r]{'level'})
469                     or test_number_is_unacceptable($number)) {
470
471                     $skipped++;
472
473                 } else {
474
475                     my $result = execute_regression_test($regression_tests[$s][$r]);
476
477                     log_result($regression_tests[$s][$r], $result, $tests);
478
479                     $successes += $result;
480                     $tests++;
481                 }
482                 $r++;
483             }
484         }
485         $failures = $tests - $successes;
486
487         log_message("Executed " . $tests . " regression tests. " .
488             'Skipped ' . $skipped . '. ' . 
489             $successes . " successes, " . $failures . " failures.");
490
491         $all_tests    += $tests;
492         $all_failures += $failures;
493         $all_successes += $successes;
494
495     }
496
497     if (get_cli_option('loops') > 1) {
498         log_message("Total: Executed " . $all_tests . " regression tests. " .
499             $all_successes . " successes, " . $all_failures . " failures.");
500     }
501 }
502
503 sub level_is_unacceptable ($) {
504     my $level = shift;
505     return ((cli_option_is_set('level') and get_cli_option('level') != $level)
506             or ($level < get_cli_option('min-level'))
507             or ($level > get_cli_option('max-level'))
508             or dependency_unsatisfied($level)
509             );
510 }
511
512 sub test_number_is_unacceptable ($) {
513     my $test_number = shift;
514     return (cli_option_is_set('test-number')
515             and get_cli_option('test-number') != $test_number)
516 }
517
518 sub dependency_unsatisfied ($) {
519
520     my $level = shift;
521     our %dependencies;
522     our @privoxy_config;
523     my $dependency_problem = 0;
524
525     if (defined ($dependencies{$level}{'config line'})) {
526
527         my $dependency = $dependencies{$level}{'config line'};
528         $dependency_problem = 1;
529
530         foreach (@privoxy_config) {
531
532              $dependency_problem = 0 if (/$dependency/);
533         }
534     }
535
536     return $dependency_problem;
537 }
538
539 sub register_dependency ($$) {
540
541     my $level = shift;
542     my $dependency = shift;
543     our %dependencies;
544
545     if ($dependency =~ /config line\s+(.*)/) {
546
547        $dependencies{$level}{'config line'} = $1;
548     }
549 }
550
551 # XXX: somewhat misleading name
552 sub execute_regression_test ($) {
553
554     my $test_ref = shift;
555     my %test = %{$test_ref};
556     my $result = 0;
557
558     if ($test{'type'} == CLIENT_HEADER_TEST) {
559
560         $result = execute_client_header_regression_test($test_ref);
561
562     } elsif ($test{'type'} == SERVER_HEADER_TEST) {
563
564         $result = execute_server_header_regression_test($test_ref);
565
566     } elsif ($test{'type'} == DUMB_FETCH_TEST
567           or $test{'type'} == TRUSTED_CGI_REQUEST) {
568
569         $result = execute_dumb_fetch_test($test_ref);
570
571     } elsif ($test{'type'} == METHOD_TEST) {
572
573         $result = execute_method_test($test_ref);
574
575     } elsif ($test{'type'} == BLOCK_TEST) {
576
577         $result = execute_block_test($test_ref);
578
579     } else {
580
581         die "Unsupported test type detected: " . $test{'type'};
582
583     }
584
585     return $result;
586 }
587
588 sub execute_method_test ($) {
589
590     my $test_ref = shift;
591     my %test = %{$test_ref};
592     my $buffer_ref;
593     my $result = 0;
594     my $status_code;
595     my $method = $test{'data'};
596
597     my $curl_parameters = '';
598     my $expected_status_code = $test{'expected-status-code'};
599
600     $curl_parameters .= '--request ' . $method . ' ';
601     # Don't complain in case about the 'missing' body
602     $curl_parameters .= '--head ' if ($method =~ /^HEAD$/i);
603
604     $curl_parameters .= PRIVOXY_CGI_URL;
605
606     $buffer_ref = get_page_with_curl($curl_parameters);
607     $status_code = get_status_code($buffer_ref);
608
609     $result = check_status_code_result($status_code, $expected_status_code);
610
611     return $result;
612 }
613
614 sub execute_dumb_fetch_test ($) {
615
616     my $test_ref = shift;
617     my %test = %{$test_ref};
618     my $buffer_ref;
619     my $result = 0;
620     my $status_code;
621
622     my $curl_parameters = '';
623     my $expected_status_code = $test{'expected-status-code'};
624
625     if (defined $test{method}) {
626         $curl_parameters .= '--request ' . $test{method} . ' ';
627     }
628     if ($test{type} == TRUSTED_CGI_REQUEST) {
629         $curl_parameters .= '--referer ' . PRIVOXY_CGI_URL . ' ';
630     }
631
632     $curl_parameters .= $test{'data'};
633
634     $buffer_ref = get_page_with_curl($curl_parameters);
635     $status_code = get_status_code($buffer_ref);
636
637     $result = check_status_code_result($status_code, $expected_status_code);
638
639     return $result;
640 }
641
642 sub execute_block_test ($) {
643
644     my $test = shift;
645     my $url = $test->{'data'};
646     my $final_results = get_final_results($url);
647
648     return defined $final_results->{'+block'};
649 }
650
651 sub get_final_results ($) {
652
653     my $url = shift;
654     my $curl_parameters = '';
655     my %final_results = ();
656     my $final_results_reached = 0;
657
658     die "Unacceptable characterss in $url" if $url =~ m@[\\'"]@;
659     $url =~ s@\s@%20@g;
660
661     $curl_parameters .= "'" . PRIVOXY_CGI_URL . 'show-url-info?url=' . $url . "'";
662
663     foreach (@{get_cgi_page_or_else($curl_parameters)}) {
664
665         $final_results_reached = 1 if (m@<h2>Final results:</h2>@);
666
667         next unless ($final_results_reached);
668         last if (m@</td>@);
669
670         if (m@<br>([-+])<a.*>([^>]*)</a>( \{.*\})@) {
671             my $action = $1.$2;
672             my $value = $3;
673             $final_results{$action} = $value;
674         }
675     }
676
677     return \%final_results;
678 }
679
680 sub check_status_code_result ($$) {
681
682     my $status_code = shift;
683     my $expected_status_code = shift;
684     my $result = 0;
685
686     if ($expected_status_code == $status_code) {
687
688         $result = 1;
689         l(LL_VERBOSE_SUCCESS,
690           "Yay. We expected status code " . $expected_status_code . ", and received: " . $status_code . '.');
691
692     } elsif (cli_option_is_set('fuzzer-feeding') and $status_code == 123) {
693
694         l(LL_VERBOSE_FAILURE,
695           "Oh well. Status code lost while fuzzing. Can't check if it was " . $expected_status_code . '.');
696
697     } else {
698
699         l(LL_VERBOSE_FAILURE,
700           "Ooops. We expected status code " . $expected_status_code . ", but received: " . $status_code . '.');
701
702     }
703     
704     return $result;
705 }
706
707 sub execute_client_header_regression_test ($) {
708
709     my $test_ref = shift;
710     my $buffer_ref;
711     my $header;
712     my $result = 0;
713
714     $buffer_ref = get_show_request_with_curl($test_ref);
715
716     $header = get_header($buffer_ref, $test_ref);
717     $result = check_header_result($test_ref, $header);
718
719     return $result;
720 }
721
722 sub execute_server_header_regression_test ($) {
723
724     my $test_ref = shift;
725     my $buffer_ref;
726     my $header;
727     my $result = 0;
728
729     $buffer_ref = get_head_with_curl($test_ref);
730
731     $header = get_server_header($buffer_ref, $test_ref);
732     $result = check_header_result($test_ref, $header);
733
734     return $result;
735 }
736
737
738 sub interpret_result ($) {
739     my $success = shift;
740     return $success ? "Success" : "Failure";
741 }
742
743 sub check_header_result ($$) {
744
745     my $test_ref = shift;
746     my $header = shift;
747
748     my %test = %{$test_ref};
749     my $expect_header = $test{'expect-header'};
750     my $success = 0;
751
752     $header =~ s@   @ @g if defined($header);
753
754     if ($expect_header eq 'NO CHANGE') {
755
756         if (defined($header) and $header eq $test{'data'}) {
757
758             $success = 1;
759
760         } else {
761
762             $header //= "REMOVAL";
763             l(LL_VERBOSE_FAILURE,
764               "Ooops. Got: " . $header . " while expecting: " . $expect_header);
765         }
766
767     } elsif ($expect_header eq 'REMOVAL') {
768
769         if (defined($header) and $header eq $test{'data'}) {
770
771             l(LL_VERBOSE_FAILURE,
772               "Ooops. Expected removal but: " . $header . " is still there.");
773
774         } else {
775
776             # XXX: Use more reliable check here and make sure
777             # the header has a different name.
778             $success = 1;
779
780         }
781
782     } elsif ($expect_header eq 'SOME CHANGE') {
783
784         if (defined($header) and not $header eq $test{'data'}) {
785
786             $success = 1;
787
788         } else {
789
790             $header //= "REMOVAL";
791             l(LL_VERBOSE_FAILURE,
792               "Ooops. Got: " . $header . " while expecting: SOME CHANGE");
793         }
794
795
796     } else {
797
798         if (defined($header) and $header eq $expect_header) {
799
800             $success = 1;
801
802         } else {
803
804             $header //= "'No matching header'"; # XXX: No header detected to be precise
805             l(LL_VERBOSE_FAILURE,
806               "Ooops. Got: " . $header . " while expecting: " . $expect_header);
807         }
808     }
809     return $success;
810 }
811
812 sub get_header_name ($) {
813
814     my $header = shift;
815
816     $header =~ s@(.*?: ).*@$1@;
817
818     return $header;
819 }
820
821 sub get_header ($$) {
822
823     our $filtered_request = '';
824
825     my $buffer_ref = shift;
826     my $test_ref = shift;
827
828     my %test = %{$test_ref};
829     my @buffer = @{$buffer_ref};
830
831     my $expect_header = $test{'expect-header'};
832
833     my $line;
834     my $processed_request_reached = 0;
835     my $read_header = 0;
836     my $processed_request = '';
837     my $header;
838     my $header_to_get;
839
840     if ($expect_header eq 'REMOVAL'
841      or $expect_header eq 'NO CHANGE'
842      or  $expect_header eq 'SOME CHANGE') {
843
844         $expect_header = $test{'data'};
845
846     }
847
848     $header_to_get = get_header_name($expect_header);
849
850     foreach (@buffer) {
851
852         # Skip everything before the Processed request
853         if (/Processed Request/) {
854             $processed_request_reached = 1;
855             next;
856         }
857         next unless $processed_request_reached;
858
859         # End loop after the Processed request
860         last if (/<\/pre>/);
861
862         # Ditch tags and leading/trailing white space.
863         s@^\s*<.*?>@@g;
864         s@\s*$@@g;
865
866         $filtered_request .=  "\n" . $_;
867          
868         if (/^$header_to_get/) {
869             $read_header = 1;
870             $header = $_;
871             last;
872         }
873     }
874
875     return $header;
876 }
877
878 sub get_server_header ($$) {
879
880     my $buffer_ref = shift;
881     my $test_ref = shift;
882
883     my %test = %{$test_ref};
884     my @buffer = @{$buffer_ref};
885
886     my $expect_header = $test{'expect-header'};
887     my $header;
888     my $header_to_get;
889
890     if ($expect_header eq 'REMOVAL'
891      or $expect_header eq 'NO CHANGE'
892      or $expect_header eq 'SOME CHANGE') {
893
894         $expect_header = $test{'data'};
895
896     }
897
898     $header_to_get = get_header_name($expect_header);
899
900     foreach (@buffer) {
901
902         # XXX: should probably verify that the request
903         # was actually answered by Fellatio.
904         if (/^$header_to_get/) {
905             $header = $_;
906             $header =~ s@\s*$@@g;
907             last;
908         }
909     }
910
911     return $header;
912 }
913
914 sub get_status_code ($) {
915
916     my $buffer_ref = shift;
917     my @buffer = @{$buffer_ref}; 
918
919     foreach (@buffer) {
920
921         if (/^HTTP\/\d\.\d (\d{3})/) {
922
923             return $1;
924
925         } else {
926
927             return '123' if cli_option_is_set('fuzzer-feeding');
928             chomp;
929             l(LL_ERROR, 'Unexpected buffer line: "' . $_ . '"');
930         }
931     }
932 }
933
934 sub get_test_keys () {
935     return ('tag', 'data', 'expect-header', 'ignore');
936 }
937
938 # XXX: incomplete
939 sub test_content_as_string ($) {
940
941     my $test_ref = shift;
942     my %test = %{$test_ref};
943
944     my $s = "\n\t";
945
946     foreach my $key (get_test_keys()) {
947         $test{$key} = 'Not set' unless (defined $test{$key});
948     }
949
950     $s .= 'Tag: ' . $test{'tag'};
951     $s .= "\n\t";
952     $s .= 'Set header: ' . $test{'data'}; # XXX: adjust for other test types
953     $s .= "\n\t";
954     $s .= 'Expected header: ' . $test{'expect-header'};
955     $s .= "\n\t";
956     $s .= 'Ignore: ' . $test{'ignore'};
957
958     return $s;
959 }
960
961 ############################################################################
962 #
963 # HTTP fetch functions
964 #
965 ############################################################################
966
967 sub check_for_curl () {
968     my $curl = CURL;
969     l(LL_ERROR, "No curl found.") unless (`which $curl`);
970 }
971
972 sub get_cgi_page_or_else ($) {
973
974     my $cgi_url = shift;
975     my $content_ref = get_page_with_curl($cgi_url);
976     my $status_code = get_status_code($content_ref);
977
978     if (200 != $status_code) {
979
980         my $log_message = "Failed to fetch Privoxy CGI Page. " .
981                           "Received status code ". $status_code .
982                           " while only 200 is acceptable.";
983
984         if (cli_option_is_set('fuzzer-feeding')) {
985
986             $log_message .= " Ignored due to fuzzer feeding.";
987             l(LL_SOFT_ERROR, $log_message)
988
989         } else {
990
991             l(LL_ERROR, $log_message);
992
993         }
994     }
995     
996     return $content_ref;
997 }
998
999 sub get_show_request_with_curl ($) {
1000
1001     our $privoxy_cgi_url;
1002     my $test_ref = shift;
1003     my %test = %{$test_ref};
1004
1005     my $curl_parameters = ' ';
1006
1007     # Enable the action to test
1008     $curl_parameters .= '-H \'X-Privoxy-Control: ' . $test{'tag'} . '\' ';
1009     # The header to filter
1010     $curl_parameters .= '-H \'' . $test{'data'} . '\' ';
1011
1012     $curl_parameters .= ' ';
1013     $curl_parameters .= $privoxy_cgi_url;
1014     $curl_parameters .= 'show-request';
1015
1016     return get_cgi_page_or_else($curl_parameters);
1017 }
1018
1019
1020 sub get_head_with_curl ($) {
1021
1022     our $fellatio_url = FELLATIO_URL;
1023     my $test_ref = shift;
1024     my %test = %{$test_ref};
1025
1026     my $curl_parameters = ' ';
1027
1028     # Enable the action to test
1029     $curl_parameters .= '-H \'X-Privoxy-Control: ' . $test{'tag'} . '\' ';
1030     # The header to filter
1031     $curl_parameters .= '-H \'X-Gimme-Head-With: ' . $test{'data'} . '\' ';
1032     $curl_parameters .= '--head ';
1033
1034     $curl_parameters .= ' ';
1035     $curl_parameters .= $fellatio_url;
1036
1037     return get_page_with_curl($curl_parameters);
1038 }
1039
1040
1041 sub get_page_with_curl ($) {
1042
1043     my $parameters = shift;
1044     my @buffer;
1045     my $curl_line = CURL;
1046     my $retries_left = get_cli_option('retries') + 1;
1047     my $failure_reason;
1048
1049     if (cli_option_is_set('privoxy-address')) {
1050         $curl_line .= ' --proxy ' . get_cli_option('privoxy-address');
1051     }
1052
1053     # We want to see the HTTP status code
1054     $curl_line .= " --include ";
1055     # Let Privoxy emit two log messages less.
1056     $curl_line .= ' -H \'Proxy-Connection:\' ' unless $parameters =~ /Proxy-Connection:/;
1057     $curl_line .= ' -H \'Connection: close\' ' unless $parameters =~ /Connection:/;
1058     # We don't care about fetch statistic.
1059     $curl_line .= " -s ";
1060     # We do care about the failure reason if any.
1061     $curl_line .= " -S ";
1062     # We want to advertise ourselves
1063     $curl_line .= " --user-agent '" . PRT_VERSION . "' ";
1064     # We aren't too patient
1065     $curl_line .= " --max-time '" . get_cli_option('max-time') . "' ";
1066
1067     $curl_line .= $parameters;
1068     # XXX: still necessary?
1069     $curl_line .= ' 2>&1';
1070
1071     l(LL_PAGE_FETCHING, "Executing: " . $curl_line);
1072
1073     do {
1074         @buffer = `$curl_line`;
1075
1076         if ($?) {
1077             $failure_reason = array_as_string(\@buffer);
1078             chomp $failure_reason;
1079             l(LL_SOFT_ERROR, "Fetch failure: '" . $failure_reason . $! ."'");
1080         }
1081     } while ($? && --$retries_left);
1082
1083     unless ($retries_left) {
1084         l(LL_ERROR,
1085           "Running curl failed " . get_cli_option('retries') .
1086           " times in a row. Last error: '" . $failure_reason . "'.");
1087     }
1088
1089     return \@buffer;
1090 }
1091
1092
1093 ############################################################################
1094 #
1095 # Log functions
1096 #
1097 ############################################################################
1098
1099 sub array_as_string ($) {
1100     my $array_ref = shift;
1101     my $string = '';
1102
1103     foreach (@{$array_ref}) {
1104         $string .= $_;
1105     }
1106
1107     return $string;
1108 }
1109
1110
1111 sub show_test ($) {
1112     my $test_ref = shift;
1113     log_message('Test is:' . test_content_as_string($test_ref));
1114 }
1115
1116 # Conditional log
1117 sub l ($$) {
1118     our $log_level;
1119     my $this_level = shift;
1120     my $message = shift;
1121
1122     return unless ($log_level & $this_level);
1123
1124     if (LL_ERROR & $this_level) {
1125         $message = 'Oh noes. ' . $message . ' Fatal error. Exiting.';
1126     }
1127
1128     log_message($message);
1129
1130     if (LL_ERROR & $this_level) {
1131         exit;
1132     }
1133 }
1134
1135 sub log_message ($) {
1136
1137     my $message = shift;
1138
1139     our $logfile;
1140     our $no_logging;
1141     our $leading_log_date;
1142     our $leading_log_time;
1143
1144     my $time_stamp = '';
1145     my ( $sec, $min, $hour, $mday, $mon, $year, $wday, $yday, $isdst ) = localtime time;
1146
1147     if ($leading_log_date || $leading_log_time) {
1148
1149         if ($leading_log_date) {
1150             $year += 1900;
1151             $mon  += 1;
1152             $time_stamp = sprintf("%i/%.2i/%.2i", $year, $mon, $mday);
1153         }
1154
1155         if ($leading_log_time) {
1156             $time_stamp .= ' ' if $leading_log_date;
1157             $time_stamp.= sprintf("%.2i:%.2i:%.2i", $hour, $min, $sec);
1158         }
1159         
1160         $message = $time_stamp . ": " . $message;
1161     }
1162
1163
1164     printf(STDERR "%s\n", $message);
1165
1166 }
1167
1168 sub log_result ($$) {
1169
1170     our $verbose_test_description;
1171     our $filtered_request;
1172
1173     my $test_ref = shift;
1174     my $result = shift;
1175     my $number = shift;
1176
1177     my %test = %{$test_ref};
1178     my $message = '';
1179
1180     $message .= interpret_result($result);
1181     $message .= " for test ";
1182     $message .= $number;
1183     $message .= '/';
1184     $message .= $test{'number'};
1185     $message .= '/';
1186     $message .= $test{'section-id'};
1187     $message .= '/';
1188     $message .= $test{'regression-test-id'};
1189     $message .= '.';
1190
1191     if ($verbose_test_description) {
1192
1193         if ($test{'type'} == CLIENT_HEADER_TEST) {
1194
1195             $message .= ' Header ';
1196             $message .= quote($test{'data'});
1197             $message .= ' and tag ';
1198             $message .= quote($test{'tag'});
1199
1200         } elsif ($test{'type'} == SERVER_HEADER_TEST) {
1201
1202             $message .= ' Request Header ';
1203             $message .= quote($test{'data'});
1204             $message .= ' and tag ';
1205             $message .= quote($test{'tag'});
1206
1207         } elsif ($test{'type'} == DUMB_FETCH_TEST) {
1208
1209             $message .= ' URL ';
1210             $message .= quote($test{'data'});
1211             $message .= ' and expected status code ';
1212             $message .= quote($test{'expected-status-code'});
1213
1214         } elsif ($test{'type'} == TRUSTED_CGI_REQUEST) {
1215
1216             $message .= ' CGI URL ';
1217             $message .= quote($test{'data'});
1218             $message .= ' and expected status code ';
1219             $message .= quote($test{'expected-status-code'});
1220
1221         } elsif ($test{'type'} == METHOD_TEST) {
1222
1223             $message .= ' HTTP method ';
1224             $message .= quote($test{'data'});
1225             $message .= ' and expected status code ';
1226             $message .= quote($test{'expected-status-code'});
1227
1228         } elsif ($test{'type'} == BLOCK_TEST) {
1229
1230             $message .= ' Supposedly-blocked URL: ';
1231             $message .= quote($test{'data'});
1232
1233         } else {
1234
1235             die "Incomplete support for test type " . $test{'type'} .  " detected.";
1236
1237         }
1238     }
1239
1240     log_message($message) unless ($result && cli_option_is_set('silent'));
1241 }
1242
1243 sub quote ($) {
1244     my $s = shift;
1245     return '\'' . $s . '\'';
1246 }
1247
1248 sub print_version () {
1249     printf PRT_VERSION . "\n" . 'Copyright (C) 2007-2008 Fabian Keil <fk@fabiankeil.de>' . "\n";
1250 }
1251
1252 sub help () {
1253
1254     our %cli_options;
1255
1256     print_version();
1257
1258     print << "    EOF"
1259
1260 Options and their default values if they have any:
1261     [--debug $cli_options{'debug'}]
1262     [--fuzzer-feeding]
1263     [--help]
1264     [--level]
1265     [--loops $cli_options{'loops'}]
1266     [--max-level $cli_options{'max-level'}]
1267     [--max-time $cli_options{'max-time'}]
1268     [--min-level $cli_options{'min-level'}]
1269     [--privoxy-address]
1270     [--retries $cli_options{'retries'}]
1271     [--silent]
1272     [--version]
1273 see "perldoc $0" for more information
1274     EOF
1275     ;
1276     exit(0);
1277 }
1278
1279 sub init_cli_options () {
1280
1281     our %cli_options;
1282     our $log_level;
1283
1284     $cli_options{'min-level'} = CLI_MIN_LEVEL;
1285     $cli_options{'max-level'} = CLI_MAX_LEVEL;
1286     $cli_options{'debug'}  = $log_level;
1287     $cli_options{'loops'}  = CLI_LOOPS;
1288     $cli_options{'max-time'}  = CLI_MAX_TIME;
1289     $cli_options{'retries'}  = CLI_RETRIES;
1290 }
1291
1292 sub parse_cli_options () {
1293
1294     our %cli_options;
1295     our $log_level;
1296
1297     init_cli_options();
1298
1299     GetOptions (
1300                 'debug=s' => \$cli_options{'debug'},
1301                 'help'     => sub { help },
1302                 'silent' => \$cli_options{'silent'},
1303                 'min-level=s' => \$cli_options{'min-level'},
1304                 'max-level=s' => \$cli_options{'max-level'},
1305                 'privoxy-address=s' => \$cli_options{'privoxy-address'},
1306                 'level=s' => \$cli_options{'level'},
1307                 'loops=s' => \$cli_options{'loops'},
1308                 'test-number=s' => \$cli_options{'test-number'},
1309                 'fuzzer-feeding' => \$cli_options{'fuzzer-feeding'},
1310                 'retries=s' => \$cli_options{'retries'},
1311                 'max-time=s' => \$cli_options{'max-time'},
1312                 'version'  => sub { print_version && exit(0) }
1313     );
1314     $log_level |= $cli_options{'debug'};
1315 }
1316
1317 sub cli_option_is_set ($) {
1318
1319     our %cli_options;
1320     my $cli_option = shift;
1321
1322     return defined $cli_options{$cli_option};
1323 }
1324
1325 sub get_cli_option ($) {
1326
1327     our %cli_options;
1328     my $cli_option = shift;
1329
1330     die "Unknown CLI option: $cli_option" unless defined $cli_options{$cli_option};
1331
1332     return $cli_options{$cli_option};
1333 }
1334
1335 sub main () {
1336
1337     init_our_variables();
1338     parse_cli_options();
1339     check_for_curl();
1340     load_regressions_tests();
1341     execute_regression_tests();
1342 }
1343
1344 main();
1345
1346 =head1 NAME
1347
1348 B<privoxy-regression-test> - A regression test "framework" for Privoxy.
1349
1350 =head1 SYNOPSIS
1351
1352 B<privoxy-regression-test> [B<--debug bitmask>] [B<--fuzzer-feeding>] [B<--help>]
1353 [B<--level level>] [B<--loops count>] [B<--max-level max-level>]
1354 [B<--max-time max-time>] [B<--min-level min-level>] B<--privoxy-address proxy-address>
1355 [B<--retries retries>] [B<--silent>] [B<--version>]
1356
1357 =head1 DESCRIPTION
1358
1359 Privoxy-Regression-Test is supposed to one day become
1360 a regression test suite for Privoxy. It's not quite there
1361 yet, however, and can currently only test client header
1362 actions and check the returned status code for requests
1363 to arbitrary URLs.
1364
1365 Client header actions are tested by requesting
1366 B<http://config.privoxy.org/show-request> and checking whether
1367 or not Privoxy modified the original request as expected.
1368
1369 The original request contains both the header the action-to-be-tested
1370 acts upon and an additional tagger-triggering header that enables
1371 the action to test.
1372
1373 =head1 CONFIGURATION FILE SYNTAX
1374
1375 Privoxy-Regression-Test's configuration is embedded in
1376 Privoxy action files and loaded through Privoxy's web interface.
1377
1378 It makes testing a Privoxy version running on a remote system easier
1379 and should prevent you from updating your tests without updating Privoxy's
1380 configuration accordingly.
1381
1382 A client-header-action test section looks like this:
1383
1384     # Set Header    = Referer: http://www.example.org.zwiebelsuppe.exit/
1385     # Expect Header = Referer: http://www.example.org/
1386     {+client-header-filter{hide-tor-exit-notation} -hide-referer}
1387     TAG:^client-header-filter\{hide-tor-exit-notation\}$
1388
1389 The example above causes Privoxy-Regression-Test to set
1390 the header B<Referer: http://www.example.org.zwiebelsuppe.exit/>
1391 and to expect it to be modified to
1392 B<Referer: http://www.example.org/>.
1393
1394 When testing this section, Privoxy-Regression-Test will set the header
1395 B<X-Privoxy-Control: client-header-filter{hide-tor-exit-notation}>
1396 causing the B<privoxy-control> tagger to create the tag
1397 B<client-header-filter{hide-tor-exit-notation}> which will finally
1398 cause Privoxy to enable the action section.
1399
1400 Note that the actions itself are only used by Privoxy,
1401 Privoxy-Regression-Test ignores them and will be happy
1402 as long as the expectations are satisfied.
1403
1404 A fetch test looks like this:
1405
1406     # Fetch Test = http://p.p/user-manual
1407     # Expect Status Code = 302
1408
1409 It tells Privoxy-Regression-Test to request B<http://p.p/user-manual>
1410 and to expect a response with the HTTP status code B<302>. Obviously that's
1411 not a very thorough test and mainly useful to get some code coverage
1412 for Valgrind or to verify that the templates are installed correctly.
1413
1414 If you want to test CGI pages that require a trusted
1415 referer, you can use:
1416
1417     # Trusted CGI Request =  http://p.p/edit-actions
1418
1419 It works like ordinary fetch tests, but sets the referer
1420 header to a trusted value.
1421
1422 If no explicit status code expectation is set, B<200> is used.
1423
1424 Additionally all tests have test levels to let the user
1425 control which ones to execute (see I<OPTIONS> below). 
1426 Test levels are either set with the B<Level> directive,
1427 or implicitly through the test type.
1428
1429 Fetch tests default to level 6, tests for trusted
1430 CGI requests to level 3 and client-header-action tests
1431 to level 1.
1432
1433 =head1 OPTIONS
1434
1435 B<--debug bitmask> Add the bitmask provided as integer
1436 to the debug settings.
1437
1438 B<--fuzzer-feeding> Ignore some errors that would otherwise
1439 cause Privoxy-Regression-Test to abort the test because
1440 they shouldn't happen in normal operation. This option is
1441 intended to be used if Privoxy-Regression-Test is only
1442 used to feed a fuzzer in which case there's a high chance
1443 that Privoxy gets an invalid request and returns an error
1444 message.
1445
1446 B<--help> Shows available command line options.
1447
1448 B<--level level> Only execute tests with the specified B<level>. 
1449
1450 B<--loop count> Loop through the regression tests B<count> times. 
1451 Useful to feed a fuzzer, or when doing stress tests with
1452 several Privoxy-Regression-Test instances running at the same
1453 time.
1454
1455 B<--max-level max-level> Only execute tests with a B<level>
1456 below or equal to the numerical B<max-level>.
1457
1458 B<--max-time max-time> Give Privoxy B<max-time> seconds
1459 to return data. Increasing the default may make sense when
1460 Privoxy is run through Valgrind, decreasing the default may
1461 make sense when Privoxy-Regression-Test is used to feed
1462 a fuzzer.
1463
1464 B<--min-level min-level> Only execute tests with a B<level>
1465 above or equal to the numerical B<min-level>.
1466
1467 B<--privoxy-address proxy-address> Privoxy's listening address.
1468 If it's not set, the value of the environment variable http_proxy
1469 will be used. B<proxy-address> has to be specified in http_proxy
1470 syntax.
1471
1472 B<--retries retries> Retry B<retries> times.
1473
1474 B<--silent> Don't log succesful test runs.
1475
1476 B<--version> Print version and exit.
1477
1478 The second dash is optional, options can be shortened,
1479 as long as there are no ambiguities.
1480
1481 =head1 PRIVOXY CONFIGURATION
1482
1483 Privoxy-Regression-Test is shipped with B<regression-tests.action>
1484 which aims to test all official client-header modifying actions
1485 and can be used to verify that the templates and the user manual
1486 files are installed correctly.
1487
1488 To use it, it has to be copied in Privoxy's configuration
1489 directory, and afterwards referenced in Privoxy's configuration
1490 file with the line:
1491
1492     actionsfile regression-tests.action
1493
1494 In general, its tests are supposed to work without changing
1495 any other action files, unless you already added lots of
1496 taggers yourself. If you are using taggers that cause problems,
1497 you might have to temporary disable them for Privoxy's CGI pages.
1498
1499 Some of the regression tests rely on Privoxy features that
1500 may be disabled in your configuration. Tests with a level below
1501 7 are supposed to work with all Privoxy configurations (provided
1502 you didn't build with FEATURE_GRACEFUL_TERMINATION).
1503
1504 Tests with level 9 require Privoxy to deliver the User Manual,
1505 tests with level 12 require the CGI editor to be enabled.
1506
1507 =head1 CAVEATS
1508
1509 Expect the configuration file syntax to change with future releases.
1510
1511 =head1 LIMITATIONS
1512
1513 As Privoxy's B<show-request> page only shows client headers,
1514 Privoxy-Regression-Test can't use it to test Privoxy actions
1515 that modify server headers.
1516
1517 As Privoxy-Regression-Test relies on Privoxy's tag feature to
1518 control the actions to test, it currently only works with
1519 Privoxy 3.0.7 or later.
1520
1521 At the moment Privoxy-Regression-Test fetches Privoxy's
1522 configuration page through I<curl>(1), therefore you have to
1523 have I<curl> installed, otherwise you won't be able to run
1524 Privoxy-Regression-Test in a meaningful way.
1525
1526 =head1 SEE ALSO
1527
1528 privoxy(1) curl(1)
1529
1530 =head1 AUTHOR
1531
1532 Fabian Keil <fk@fabiankeil.de>
1533
1534 =cut