Add Privoxy-Regression-Test 0.2 and a few regression tests.
authorFabian Keil <fk@fabiankeil.de>
Fri, 18 Jan 2008 19:33:00 +0000 (19:33 +0000)
committerFabian Keil <fk@fabiankeil.de>
Fri, 18 Jan 2008 19:33:00 +0000 (19:33 +0000)
regression-tests.action [new file with mode: 0644]
tools/privoxy-regression-test.pl [new file with mode: 0755]

diff --git a/regression-tests.action b/regression-tests.action
new file mode 100644 (file)
index 0000000..040540b
--- /dev/null
@@ -0,0 +1,689 @@
+#############################################################################
+# $Id: regression-tests.action,v 1.52 2008/01/17 19:38:17 fk Exp $
+#############################################################################
+#
+# This is a configuration file for Privoxy-Regression-Test.
+#
+# After referencing it in your Privoxy configuration both Privoxy and
+# Privoxy-Regression-Test should be good to go.
+#
+#############################################################################
+#
+# Copyright (c) 2007-2008 Fabian Keil <fk@fabiankeil.de>
+#
+# Permission to use, copy, modify, and distribute this software for any
+# purpose with or without fee is hereby granted, provided that the above
+# copyright notice and this permission notice appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+#
+#############################################################################
+
+{{settings}}
+for-privoxy-version=3.0.7
+
+#######################################################
+# Enable taggers to activate the tests on demand
+# and suppress hiding the User-Agent for
+# Privoxy-Regression-Test to save log space.
+#######################################################
+{\
+ +client-header-tagger{user-agent} \
+ +client-header-tagger{privoxy-control} \
+ +client-header-filter{privoxy-control} \
+}
+config.privoxy.org/
+p.p/
+
+{-hide-user-agent}
+TAG:^User-Agent: Privoxy-Regression-Test
+
+#######################################################
+# Test accept-language{}.
+#######################################################
+
+# Set Header    = Accept-Language: de-de
+# Expect Header = Accept-Language: en-gb
+{+hide-accept-language{en-gb}}
+TAG:^hideaccept-language\{en-gb\}$
+
+# Set Header    = Accept-Language: de-de
+# Expect Header = REMOVAL
+{+hide-accept-language{block}}
+TAG:^hide-accept-language\{block\}$
+
+#######################################################
+# Sections for hide-referrer{} to test:
+#
+# 1) conditional-block
+# 2) conditional-forge
+# 3) forge
+# 4) block
+# 5) a parameter that looks like a valid fake referrer 
+# 6) a parameter that looks like an invalid fake referrer
+#######################################################
+
+# Set Header    = Referer: http://www.example.org/foo 
+# Expect Header = REMOVAL
+#
+# Set Header    = Referer: http://p.p/foo
+# Expect Header = NO CHANGE
+{+hide-referrer{conditional-block}}
+TAG:^hide-referrer\{conditional-block\}$
+
+# Set Header    = Referer: http://www.example.org/foo 
+# Expect Header = Referer: http://p.p/
+#
+# Set Header    = Referer: http://p.p/foo
+# Expect Header = NO CHANGE
+{+hide-referrer{conditional-forge}}
+TAG:^hide-referrer\{conditional-forge\}$
+
+# Set Header    = Referer: http://www.example.org/foo 
+# Expect Header = Referer: http://p.p/
+{+hide-referrer{forge}}
+TAG:^hide-referrer\{forge\}$
+
+# Set Header    = Referer: http://www.example.org/foo 
+# Expect Header = REMOVAL
+{+hide-referrer{block}}
+TAG:^hide-referrer\{block\}$
+
+# Set Header    = Referer: http://www.example.org/foo 
+# Expect Header = Referer: invalid
+{+hide-referrer{invalid}}
+TAG:^hide-referrer\{invalid\}$
+
+# Set Header    = Referer: http://www.example.org/asdf
+# Expect Header = Referer: http://www.privoxy.org/
+{+hide-referrer{http://www.privoxy.org/}}
+TAG:^hide-referrer\{http://www.privoxy.org/\}$
+
+#{+hide-referrer{}}
+#TAG:^hide-referrer\{\}$
+
+#######################################################
+# Test hide-user-agent{}.
+#######################################################
+
+# Set Header    = User-Agent: Mozilla/5.0 (X11; U; NetBSD i386; de-CH; rv:1.8.1.6) Gecko/20070806 Firefox/2.0.0.6
+# Expect Header = User-Agent: Mozilla/5.0 (X11; U; FreeBSD alpha; en-GB; rv:1.8.1.6) Gecko/20070913 Firefox/2.0.0.6
+{+hide-user-agent{Mozilla/5.0 (X11; U; FreeBSD alpha; en-GB; rv:1.8.1.6) Gecko/20070913 Firefox/2.0.0.6}}
+TAG:^hide-user-agent\{Mozilla/5\.0 \(X11; U; FreeBSD alpha; en-GB; rv:1\.8\.1\.6\) Gecko/20070913 Firefox/2\.0\.0\.6\}$
+
+# XXX: Check the code that is tested here.
+# Set Header = ua-blah: blah
+# Expect Header = REMOVAL
+{+hide-user-agent{block}}
+TAG:^hide-user-agent{block}$
+
+# Set Header = ua-blah: blah
+# Expect Header = NO CHANGE
+{-hide-user-agent{}}
+TAG:^-hide-user-agent{block}$
+
+
+#######################################################
+# Test add-header{}.
+#######################################################
+
+# Set Header    = X-Whatever: foo
+# Expect Header = X-Custom-Header: yes, please
+
+{+add-header{X-Custom-Header: yes, please}}
+TAG:^add-header\{X-Custom-Header: yes, please\}$
+
+#######################################################
+# Test client-header-filter{hide-tor-exit-notation}.
+#######################################################
+
+# Set Header    = Referer: http://p.p.zwiebelsuppe.exit/
+# Expect Header = Referer: http://p.p/
+#
+# Set Header    = Referer: http://p.p.zwiebelsuppe.exit/foo/bar/baaz/
+# Expect Header = Referer: http://p.p/foo/bar/baaz/
+#
+# Set Header    = Referer: http://p.p/
+# Expect Header = NO CHANGE
+#
+# Set Header    = Referer: http://config.privoxy.org.zwiebelsuppe.exit/foo/bar/baaz.html
+# Expect Header = Referer: http://config.privoxy.org/foo/bar/baaz.html
+#
+# Set Header    = Host: p.p.zwiebelsuppe.exit
+# Expect Header = Host: p.p
+#
+# Set Header    = Host: p.p
+# Expect Header = NO CHANGE
+
+{+client-header-filter{hide-tor-exit-notation} -hide-referer}
+TAG:^client-header-filter\{hide-tor-exit-notation\}$
+
+#######################################################
+# Test crunch-client-header{}.
+#######################################################
+
+# Set Header    = Content-Type: text/html
+# Expect Header = REMOVAL
+#
+# Set Header    = Content-Type: text/html; charset=4711
+# Expect Header = REMOVAL
+#
+# Set Header    = Content-Type: text/plain
+# Expect Header = NO CHANGE
+
+{+crunch-client-header{text/html}}
+TAG:^crunch-client-header\{text/plain\}$
+
+
+#######################################################
+# Test crunch-if-none-match.
+#######################################################
+
+# Set Header    = If-None-Match: 8987afd239d2093kd2309kd
+# Expect Header = REMOVAL
+
+# Set Header    = If-None-Match: 82c3cb50c984ef11b1fed749949b2a16
+# Expect Header = REMOVAL
+
+# Set Header    = If-Modified-Since: Thu, 04 Oct 2007 09:56:35 GMT
+# Expect Header = NO CHANGE
+
+{+crunch-if-none-match -hide-if-modified-since}
+TAG:^crunch-if-none-match$
+
+#######################################################
+# Test hide-if-modified-since
+#######################################################
+
+# Set Header    = If-Modified-Since: Thu, 04 Oct 2007 09:56:35 GMT
+# Expect Header = REMOVAL
+#
+# Set Header    = If-None-Match: 82c3cb50c984ef11b1fed749949b2a16
+# Expect Header = NO CHANGE
+
+{+hide-if-modified-since{block} -crunch-if-none-match}
+TAG:^hide-if-modified-since\{block\}$
+
+# Set Header    = If-Modified-Since: Gee, this date is invalid
+# Expect Header = REMOVAL
+# Set Header    = If-Modified-Since: Thu, 04 Oct 2007 09:56:35 GMT
+# Expect Header = SOME CHANGE
+
+{+hide-if-modified-since{-60} -crunch-if-none-match}
+TAG:^hide-if-modified-since\{-60\}$
+
+# Set Header    = If-Modified-Since: Gee, this date is invalid
+# Expect Header = REMOVAL
+# Set Header    = If-Modified-Since: Thu, 04 Oct 2007 09:56:35 GMT
+# Expect Header = SOME CHANGE
+
+{+hide-if-modified-since{+60} -crunch-if-none-match}
+TAG:^hide-if-modified-since\{\+60\}$
+
+# Set Header    = If-Modified-Since: Gee, this date is invalid
+# Expect Header = REMOVAL
+# Set Header    = If-Modified-Since: Thu, 04 Oct 2007 09:56:35 GMT
+# Expect Header = SOME CHANGE
+
+{+hide-if-modified-since{60} -crunch-if-none-match}
+TAG:^hide-if-modified-since\{60\}$
+
+# Set Header    = If-Modified-Since: Gee, this date is invalid
+# Expect Header = REMOVAL
+# Set Header    = If-Modified-Since: Thu, 04 Oct 2007 09:56:35 GMT
+# Expect Header = NO CHANGE
+
+{+hide-if-modified-since{+0} -crunch-if-none-match}
+TAG:^hide-if-modified-since\{\+0\}$
+
+# Set Header    = If-Modified-Since: Gee, this date is invalid
+# Expect Header = REMOVAL
+# Set Header    = If-Modified-Since: Thu, 04 Oct 2007 09:56:35 GMT
+# Expect Header = NO CHANGE
+
+{+hide-if-modified-since{-0} -crunch-if-none-match}
+TAG:^hide-if-modified-since\{-0\}$
+
+# Set Header    = If-Modified-Since: Gee, this date is invalid
+# Expect Header = REMOVAL
+# Set Header    = If-Modified-Since: Thu, 04 Oct 2007 09:56:35 GMT
+# Expect Header = NO CHANGE
+
+{+hide-if-modified-since{0} -crunch-if-none-match}
+TAG:^hide-if-modified-since\{0\}$
+
+# Set Header    = If-Modified-Since: Gee, this date is invalid
+# Expect Header = REMOVAL
+# Set Header    = If-Modified-Since: Thu, 04 Oct 2007 09:56:35 GMT
+# Expect Header = NO CHANGE
+
+{+hide-if-modified-since{NaN} -crunch-if-none-match}
+TAG:^hide-if-modified-since\{NaN\}$
+
+
+#######################################################
+# Test crunch-outgoing-cookies
+#######################################################
+
+# Set Header    = If-Modified-Since: Gee, this date is invalid
+# Expect Header = NO CHANGE
+#
+# Set Header    = Cookie: PREF=ID=6cf0abd34262:TM=117335617:LM=1617:S=jZypyJ7LPiwFi1_
+# Expect Header = REMOVAL
+{\
+ +crunch-outgoing-cookies \
+ -crunch-incoming-cookies \
+ -session-cookies-only    \
+ -hide-if-modified-since  \
+}
+TAG:^crunch-outgoing-cookies$
+
+#######################################################
+# Test session-cookies-only
+#
+# XXX: pretty useless as session-cookies-only doesn't
+# affect client headers.
+#######################################################
+
+# Set Header    = Cookie: NSC_gffe-iuuq-mc-wtfswfs=8efb330d3660;expires=Thu, 04-Oct-07 19:11:34 GMT;path=/
+# Expect Header = NO CHANGE
+#
+# Set Header    = Cookie: PREF=ID=6cf0abd34262:TM=117335617:LM=1617:S=jZypyJ7LPiwFi1_
+# Expect Header = NO CHANGE
+{\
+ -crunch-outgoing-cookies \
+ -crunch-incoming-cookies \
+ +session-cookies-only    \
+ -hide-if-modified-since  \
+}
+TAG:^session-cookies-only$
+
+#######################################################
+# Test hide-forwarded-for-headers
+#######################################################
+
+# Set Header    = X-Forwarded-For: 10.0.0.1
+# Expect Header = REMOVAL
+{\
+ +hide-forwarded-for-headers    \
+}
+TAG:^hide-forwarded-for-headers$
+
+# Set Header    = X-Forwarded-For: 10.0.0.1
+# Expect Header = NO CHANGE
+{\
+ -hide-forwarded-for-headers    \
+}
+TAG:^-hide-forwarded-for-headers$
+
+#######################################################
+# Test hide-from-header
+#######################################################
+
+# Set Header    = From: schneewitchen@example.org
+# Expect Header = REMOVAL
+{\
+ +hide-from-header{block}\
+}
+TAG:^hide-from-header\{block\}$
+
+# Set Header    = From: schneewitchen@example.org
+# Expect Header = From: siebenzwerge@example.org
+{\
+ +hide-from-header{siebenzwerge@example.org}\
+}
+TAG:^hide-from-header\{siebenzwerge@example.org\}$
+
+#######################################################
+# Test prevent-compression
+#######################################################
+
+# Set Header    = Accept-Encoding: gzip, deflate
+# Expect Header = REMOVAL
+#
+# Set Header    = Accept-Encoding: gzip
+# Expect Header = REMOVAL
+#
+# Set Header    = Accept-Encoding: deflate
+# Expect Header = REMOVAL
+{\
+ +prevent-compression\
+}
+TAG:^prevent-compression$
+
+#######################################################
+# Test send-wafer.
+#######################################################
+
+# Set Header    = X-Does-Not-Matter: Foo bar
+# Expect Header = Cookie: Tracking+me+is+easy+due+to+my+stupid+wafer+cookie
+{\
+ +send-wafer{Tracking me is easy due to my stupid wafer cookie}\
+ -send-vanilla-wafer \
+}
+TAG:^send-wafer\{Tracking me is easy due to my stupid wafer cookie\}$
+
+#######################################################
+# Test send-vanilla-wafer.
+#######################################################
+
+# Set Header    = X-Does-Not-Matter: Foo bar
+# Expect Header = Cookie: NOTICE=TO_WHOM_IT_MAY_CONCERN_Do_not_send_me_any_copyrighted_information_other_than_the_document_that_I_am_requesting_or_any_of_its_necessary_components._In_particular_do_not_send_me_any_cookies_that_are_subject_to_a_claim_of_copyright_by_anybody._Take_notice_that_I_refuse_to_be_bound_by_any_license_condition_(copyright_or_otherwise)_applying_to_any_cookie._
+{\
+ +send-vanilla-wafer \
+ -send-wafer \
+}
+TAG:^send-vanilla-wafer$
+
+#######################################################
+# Test content filters which could cause problems with
+# range requests.
+#######################################################
+
+# Set Header    = Range: bytes=1234-5678
+# Expect Header = REMOVAL
+# Set Header    = If-Range: bytes=1234-5678
+# Expect Header = REMOVAL
+# Set Header    = Request-Range: bytes=1234-5678
+# Expect Header = REMOVAL
+{\
+ +deanimate-gifs{last} \
+ -filter \
+ -inspect-jpegs \
+ -kill-popups \
+}
+TAG:^deanimate-gifs\{last\}$
+
+# Set Header    = Range: bytes=1234-5678
+# Expect Header = REMOVAL
+# Set Header    = If-Range: bytes=1234-5678
+# Expect Header = REMOVAL
+# Set Header    = Request-Range: bytes=1234-5678
+# Expect Header = REMOVAL
+{\
+ -deanimate-gifs \
+ +filter{banners-by-size} \
+ -inspect-jpegs \
+ -kill-popups \
+}
+TAG:^filter\{banners-by-size\}$
+
+# Set Header    = Range: bytes=1234-5678
+# Expect Header = REMOVAL
+# Set Header    = If-Range: bytes=1234-5678
+# Expect Header = REMOVAL
+# Set Header    = Request-Range: bytes=1234-5678
+# Expect Header = REMOVAL
+{\
+ -deanimate-gifs \
+ -filter \
+ +inspect-jpegs \
+ -kill-popups \
+}
+TAG:^filter\{banners-by-size\}$
+
+# Set Header    = Range: bytes=1234-5678
+# Expect Header = REMOVAL
+# Set Header    = If-Range: bytes=1234-5678
+# Expect Header = REMOVAL
+# Set Header    = Request-Range: bytes=1234-5678
+# Expect Header = REMOVAL
+{\
+ -deanimate-gifs \
+ -filter \
+ -inspect-jpegs \
+ +kill-popups \
+}
+TAG:^kill-popups$
+
+# Set Header    = Range: bytes=1234-5678
+# Expect Header = NO CHANGE
+# Set Header    = If-Range: bytes=1234-5678
+# Expect Header = NO CHANGE
+# Set Header    = Request-Range: bytes=1234-5678
+# Expect Header = NO CHANGE
+{\
+ -deanimate-gifs \
+ -filter \
+ -inspect-jpegs \
+ -kill-popups \
+}
+TAG:^no-content-filter$
+
+# Set Header    = Connection: keep-alive
+# Expect Header = Connection: close
+# Set Header    = Connection:
+# Expect Header = Connection: close
+{}
+TAG:^Connection: close$
+
+# XXX: Removing a header by not specifying a value is
+# an inherited curl feature and could be viewed as a
+# bug as far as Privoxy-Regression-Test is concerned.
+#
+# Set Header    = Host:
+# Expect Header = Host: p.p
+{}
+TAG:^No Host header$
+
+# Set Header    = Host: whatever.example.org
+# Expect Header = NO CHANGE
+{}
+TAG:^Host header other than the target host$
+
+# XXX: check the RFC to use a real value
+# Set Header = Keep-Alive: Yes
+# Expect Header = REMOVAL
+{}
+TAG:^Keep-Alive header removal$
+
+# XXX: check the RFC to use a real value
+# Set Header = proxy-connection: keep-alive
+# Expect Header = REMOVAL
+{}
+TAG:^Proxy-Connection removal$
+
+# Set Header = proxy-connection: keep-alive
+# Expect Header = REMOVAL
+{}
+TAG:^Proxy-Connection removal$
+
+# These are somewhat redundant when testing with
+# GET requests, but I want to remember then when
+# TRACE requests are supported.
+#
+# Set Header = Max-Forwards: 0
+# Expect Header = NO CHANGE
+# Set Header = Max-Forwards: 1
+# Expect Header = NO CHANGE
+# Set Header = Max-Forwards: -1
+# Expect Header = NO CHANGE
+# Set Header = Max-Forwards: 3
+# Expect Header = NO CHANGE
+{}
+TAG:^Max-Forwards header without TRACE method$
+
+################################################################
+#
+# Fairly dumb tests for Privoxy CGI pages.
+#
+# These are mainly useful for checking for memory leaks
+# with Valgrind or whether or not the user manual is installed
+# correctly and are unlikely to actually detect any
+#
+# Note that if "Expect Status Code" is missing, 200 is implied.
+#
+################################################################
+
+# Fetch Test = http://p.p/
+# Will fail if compiled with FEATURE_GRACEFUL_TERMINATION
+# Fetch Test = http://p.p/die
+# Expect Status Code = 404
+# Fetch Test = http://p.p/show-status
+# Fetch Test = http://p.p/show-version
+# Fetch Test = http://p.p/show-request
+# Fetch Test = http://p.p/show-url-info
+# Fetch Test = http://p.p/show-url-info?url=www.privoxy.org%2F
+# Fetch Test = http://p.p/show-url-info?url=http:%2F%2Fwww.privoxy.org%2F
+# Fetch Test = http://p.p/show-url-info?url=https:%2F%2Fwww.privoxy.org%2F
+# Fetch Test = http://p.p/show-url-info?url=
+# Fetch Test = http://p.p/show-url-info?url=%2F
+# Fetch Test = http://p.p/toggle
+# Fetch Test = http://p.p/edit-actions
+# Fetch Test = http://p.p/eaa
+# Fetch Test = http://p.p/eau
+# Fetch Test = http://p.p/ear
+# Fetch Test = http://p.p/eal
+# Fetch Test = http://p.p/eafu
+# Fetch Test = http://p.p/eas
+# Fetch Test = http://p.p/easa
+# Fetch Test = http://p.p/easr
+# Fetch Test = http://p.p/eass
+# Fetch Test = http://p.p/edit-actions-for-url
+# Fetch Test = http://p.p/edit-actions-list
+# Fetch Test = http://p.p/edit-actions-submit
+# Fetch Test = http://p.p/edit-actions-url
+# Fetch Test = http://p.p/edit-actions-url-form
+# Fetch Test = http://p.p/edit-actions-add-url
+# Fetch Test = http://p.p/edit-actions-add-url-form
+# Fetch Test = http://p.p/edit-actions-remove-url
+# Fetch Test = http://p.p/edit-actions-remove-url-form
+# Fetch Test = http://p.p/edit-actions-section-add
+# Fetch Test = http://p.p/edit-actions-section-remove
+# Fetch Test = http://p.p/edit-actions-section-swap
+# Fetch Test = http://p.p/error-favicon.ico
+# Fetch Test = http://p.p/favicon.ico
+# Fetch Test = http://p.p/robots.txt
+# Fetch Test = http://p.p/send-banner
+# Fetch Test = http://p.p/send-stylesheet
+# Fetch Test = http://p.p/t
+# Trusted CGI Request = http://p.p/edit-actions
+# Expect Status Code = 302
+# Level = 12 # Depends on the CGI editor being enabled
+# Fetch Test = http://p.p/does-not-exist
+# Expect Status Code = 404
+# Trusted CGI Request = http://p.p/eaa
+# Trusted CGI Request = http://p.p/eau
+# Trusted CGI Request = http://p.p/ear
+# Trusted CGI Request = http://p.p/eal
+# Trusted CGI Request = http://p.p/eafu
+# Trusted CGI Request = http://p.p/eas
+# Trusted CGI Request = http://p.p/easa
+# Trusted CGI Request = http://p.p/easr
+# Trusted CGI Request = http://p.p/eass
+# Trusted CGI Request = http://p.p/edit-actions-for-url
+# Trusted CGI Request = http://p.p/edit-actions-list
+# Trusted CGI Request = http://p.p/edit-actions-submit
+# Trusted CGI Request = http://p.p/edit-actions-url
+# Trusted CGI Request = http://p.p/edit-actions-url-form
+# Trusted CGI Request = http://p.p/edit-actions-add-url
+# Trusted CGI Request = http://p.p/edit-actions-add-url-form
+# Trusted CGI Request = http://p.p/edit-actions-remove-url
+# Trusted CGI Request = http://p.p/edit-actions-remove-url-form
+# Trusted CGI Request = http://p.p/edit-actions-section-add
+# Trusted CGI Request = http://p.p/edit-actions-section-remove
+# Trusted CGI Request = http://p.p/edit-actions-section-swap
+# Trusted CGI Request = http://p.p/send-stylesheet
+
+# The following tests depend on Privoxy being configured to deliver the user manual
+
+# Fetch Test = http://p.p/user-manual
+# Expect Status Code = 302
+# Level = 9
+# Fetch Test = http://p.p/user-manual/
+# Level = 9
+# Fetch Test = http://p.p/user-manual/actions-file.html
+# Level = 9
+# Fetch Test = http://p.p/user-manual/appendix.html
+# Level = 9
+# Fetch Test = http://p.p/user-manual/config.html
+# Level = 9
+# Fetch Test = http://p.p/user-manual/configuration.html
+# Level = 9
+# Fetch Test = http://p.p/user-manual/contact.html
+# Level = 9
+# Fetch Test = http://p.p/user-manual/copyright.html
+# Level = 9
+# Fetch Test = http://p.p/user-manual/files-in-use.jpg
+# Level = 9
+# Fetch Test = http://p.p/user-manual/filter-file.html
+# Level = 9
+# Fetch Test = http://p.p/user-manual/index.html
+# Level = 9
+# Fetch Test = http://p.p/user-manual/installation.html
+# Level = 9
+# Fetch Test = http://p.p/user-manual/introduction.html
+# Level = 9
+# Fetch Test = http://p.p/user-manual/p_doc.css
+# Level = 9
+# Fetch Test = http://p.p/user-manual/proxy2.jpg
+# Level = 9
+# Fetch Test = http://p.p/user-manual/proxy_setup.jpg
+# Level = 9
+# Fetch Test = http://p.p/user-manual/quickstart.html
+# Level = 9
+# Fetch Test = http://p.p/user-manual/seealso.html
+# Level = 9
+# Fetch Test = http://p.p/user-manual/startup.html
+# Level = 9
+# Fetch Test = http://p.p/user-manual/templates.html
+# Level = 9
+# Fetch Test = http://p.p/user-manual/upgradersnote.html
+# Level = 9
+# Fetch Test = http://p.p/user-manual/whatsnew.html
+# Level = 9
+
+
+# Method Test = OPTIONS
+# Method Test = GET
+# Method Test = HEAD
+# Method Test = POST
+# Method Test = PUT
+# Method Test = DELETE
+# Method Test = OPTIONS
+# Method Test = TRACE
+# Method Test = CONNECT
+# Method Test = PROPFIND
+# Method Test = PROPPATCH
+# Method Test = MOVE
+# Method Test = COPY
+# Method Test = MKCOL
+# Method Test = LOCK
+# Method Test = UNLOCK
+# Method Test = BCOPY
+# Method Test = BMOVE
+# Method Test = BDELETE
+# Method Test = BPROPFIND
+# Method Test = BPROPPATCH
+# Method Test = SUBSCRIBE
+# Method Test = UNSUBSCRIBE
+# Method Test = NOTIFY
+# Method Test = POLL
+# Method Test = VERSION-CONTROL
+# Method Test = REPORT
+# Method Test = CHECKOUT
+# Method Test = CHECKIN
+# Method Test = UNCHECKOUT
+# Method Test = MKWORKSPACE
+# Method Test = UPDATE
+# Method Test = LABEL
+# Method Test = MERGE
+# Method Test = BASELINE-CONTROL
+# Method Test = MKACTIVITY
+# Method Test = PRIVOXY-REGRESSION-TEST-IN-THE-HOUSE
+# Expect Status Code = 400
+
+{+block}
+config.privoxy.org:1-/
+p.p:1-/
+
+{-block}
+config.privoxy.org:3,79-81/
+p.p:3,22,79-81/
+
diff --git a/tools/privoxy-regression-test.pl b/tools/privoxy-regression-test.pl
new file mode 100755 (executable)
index 0000000..8890394
--- /dev/null
@@ -0,0 +1,1480 @@
+#!/usr/bin/perl
+
+############################################################################
+#
+# Privoxy-Regression-Test
+#
+# A regression test "framework" for Privoxy. For documentation see:
+# perldoc privoxy-regression-test.pl
+#
+# $Id: privoxy-regression-test.pl,v 1.102 2008/01/18 18:30:25 fk Exp $
+#
+# Wish list:
+#
+# - Update documentation
+# - Validate HTTP times.
+# - Understand default.action.master comment syntax
+#   and verify that we actually block and unblock what
+#   the comments claim we do.
+# - Implement a HTTP_VERSION directive or allow to
+#   specify whole request lines.
+# - Support filter regression tests.
+# - Add option to fork regression tests and run them in parallel,
+#   possibly optional forever.
+# - Document magic Expect Header values
+# - Internal fuzz support?
+#
+# Copyright (c) 2007-2008 Fabian Keil <fk@fabiankeil.de>
+#
+# Permission to use, copy, modify, and distribute this software for any
+# purpose with or without fee is hereby granted, provided that the above
+# copyright notice and this permission notice appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+#
+############################################################################
+
+use warnings;
+use strict;
+use Getopt::Long;
+
+use constant {
+               PRT_VERSION                => 'Privoxy-Regression-Test 0.2',
+              CURL                       => 'curl',
+
+               # CLI option defaults
+              CLI_RETRIES  => 1,
+              CLI_LOOPS    => 1,
+              CLI_MAX_TIME => 5,
+              CLI_MIN_LEVEL => 0,
+              CLI_MAX_LEVEL => 25,
+
+               PRIVOXY_CGI_URL => 'http://p.p/',
+               FELLATIO_URL    => 'http://10.0.0.1:8080/',
+               LEADING_LOG_DATE => 1,
+               LEADING_LOG_TIME => 1,
+
+               DEBUG_LEVEL_FILE_LOADING    => 0,
+               DEBUG_LEVEL_PAGE_FETCHING   => 0,
+
+               VERBOSE_TEST_DESCRIPTION    => 1,
+
+               DEBUG_LEVEL_VERBOSE_FAILURE => 1,
+               # XXX: Only partly implemented and mostly useless.
+               DEBUG_LEVEL_VERBOSE_SUCCESS => 0,
+               DEBUG_LEVEL_STATUS          => 1,
+
+               # Internal use, don't modify
+               # Available debug bits:
+               LL_ERROR                   =>  1,
+               LL_VERBOSE_FAILURE         =>  2,
+               LL_PAGE_FETCHING           =>  4,
+               LL_FILE_LOADING            =>  8,
+               LL_VERBOSE_SUCCESS         => 16,
+               LL_STATUS                  => 32,
+               LL_SOFT_ERROR              => 64,
+
+               CLIENT_HEADER_TEST         =>  1,
+               SERVER_HEADER_TEST         =>  2,
+               DUMB_FETCH_TEST            =>  3,
+               METHOD_TEST                =>  4,
+               TRUSTED_CGI_REQUEST        =>  6,
+};
+
+sub init_our_variables () {
+
+    our $leading_log_time = LEADING_LOG_TIME;
+    our $leading_log_date = LEADING_LOG_DATE;
+
+    our $privoxy_cgi_url  = PRIVOXY_CGI_URL;
+
+    our $verbose_test_description = VERBOSE_TEST_DESCRIPTION;
+
+    our $log_level = get_default_log_level();
+
+}
+
+sub get_default_log_level () {
+    
+    my $log_level = 0;
+
+    $log_level |= LL_FILE_LOADING    if DEBUG_LEVEL_FILE_LOADING;
+    $log_level |= LL_PAGE_FETCHING   if DEBUG_LEVEL_PAGE_FETCHING;
+    $log_level |= LL_VERBOSE_FAILURE if DEBUG_LEVEL_VERBOSE_FAILURE;
+    $log_level |= LL_VERBOSE_SUCCESS if DEBUG_LEVEL_VERBOSE_SUCCESS;
+    $log_level |= LL_STATUS          if DEBUG_LEVEL_STATUS;
+
+    # These are intended to be always on.
+    $log_level |= LL_SOFT_ERROR;
+    $log_level |= LL_ERROR;
+
+    return $log_level;
+}
+
+############################################################################
+#
+# File loading functions
+#
+############################################################################
+
+sub parse_tag ($) {
+
+    my $tag = shift;
+
+    # Remove anchors
+    $tag =~ s@[\$\^]@@g;
+    # Unescape brackets and dots
+    $tag =~ s@\\(?=[{}().+])@@g;
+
+    # log_message("Parsed tag: " . $tag);
+
+    check_for_forbidden_characters($tag);
+
+    return $tag;
+}
+
+sub check_for_forbidden_characters ($) {
+
+    my $tag = shift; # XXX: also used to check values though.
+    my $allowed = '[-=\dA-Za-z{}:.\/();\s,+@"_%\?&]';
+
+    unless ($tag =~ m/^$allowed*$/) {
+        my $forbidden = $tag;
+        $forbidden =~ s@^$allowed*(.).*@$1@;
+
+        l(LL_ERROR, "'" . $tag . "' contains character '" . $forbidden. "' which is unacceptable.");
+    }
+}
+
+sub load_regressions_tests () {
+
+    our $privoxy_cgi_url;
+    my @actionfiles;
+    my $curl_url        = '';
+    my $file_number = 0;
+
+    $curl_url .= $privoxy_cgi_url;
+    $curl_url .= 'show-status';
+
+    l(LL_STATUS, "Asking Privoxy for the number of action files available ...");
+
+    foreach (@{get_cgi_page_or_else($curl_url)}) {
+
+        if (/<td>(.*?)<\/td><td class=\"buttons\"><a href=\"\/show-status\?file=actions&amp;index=(\d+)\">/) {
+
+            my $url = $privoxy_cgi_url . 'show-status?file=actions&index=' . $2;
+            $actionfiles[$file_number++] = $url;
+        }
+    }
+
+    l(LL_FILE_LOADING, "Recognized " . @actionfiles . " actions files");
+
+    load_action_files(\@actionfiles);
+}
+
+sub token_starts_new_test ($) {
+
+    my $token = shift;
+    my @new_test_directives =
+        ('set header', 'fetch test', 'trusted cgi request', 'request header', 'method test');
+
+    foreach my $new_test_directive (@new_test_directives) {
+        return 1 if $new_test_directive eq $token;
+    }
+    return 0;
+
+}
+
+sub tokenize ($) {
+
+    my ($token, $value) = (undef, undef);
+
+    # Remove leading and trailing white space.
+    s@^\s*@@;
+    s@\s*$@@;
+
+    # Reverse HTML-encoding
+    # XXX: Seriously imcomplete. 
+    s@&quot;@"@g;
+
+    # Tokenize
+    if (/^\#\s*([^=]*?)\s*[=]\s*(.*?)\s*$/) {
+
+        $token = $1;
+        $token =~ tr/[A-Z]/[a-z]/;
+        $value = $2;
+
+    } elsif (/^TAG\s*:(.*)$/) {
+
+        $token = 'tag';
+        $value = $1;
+
+    }
+
+    return ($token, $value);
+}
+
+sub load_action_files ($) {
+
+    # initialized here
+    our %actions;
+    our @regression_tests;
+
+    my $actionfiles_ref = shift;
+    my @actionfiles = @{$actionfiles_ref};
+    my $number;
+
+    my $si = 0;  # Section index
+    my $ri = -1; # Regression test index
+    my $number_of_regression_tests = 0;
+
+    my $ignored = 0;
+
+    l(LL_STATUS, "Loading regression tests from action file(s) delivered by Privoxy.");
+
+    for my $file_number (0 .. @actionfiles - 1) {
+
+        my $curl_url = ' "' . $actionfiles[$file_number] . '"';
+        my $actionfile = undef;
+
+        foreach (@{get_cgi_page_or_else($curl_url)}) {
+
+            my $no_checks = 0;
+            chomp;
+            
+            if (/<h2>Contents of Actions File (.*?)</) {
+                $actionfile = $1;
+                next;
+            }
+            next unless defined $actionfile;
+
+            last if (/<\/pre>/);
+
+            my ($token, $value) = tokenize($_);
+
+            next unless defined $token;
+
+            # Load regression tests
+
+            if (token_starts_new_test($token)) {
+
+                # Beginning of new regression test.
+                $ri++;
+                $number_of_regression_tests++;
+
+                l(LL_FILE_LOADING, "Regression test " . $number_of_regression_tests . " (section:" . $si . "):");
+
+                if ($token eq 'set header') {
+
+                    l(LL_FILE_LOADING, "Header to set: " . $value);
+                    $regression_tests[$si][$ri]{'type'} = CLIENT_HEADER_TEST;
+                    # Implicit default
+                    $regression_tests[$si][$ri]{'level'} = CLIENT_HEADER_TEST;
+
+                } elsif ($token eq 'request header') {
+
+                    l(LL_FILE_LOADING, "Header to request: " . $value);
+                    $regression_tests[$si][$ri]{'type'} = SERVER_HEADER_TEST;
+                    # Implicit default
+                    $regression_tests[$si][$ri]{'expected-status-code'} = 200;
+                    $regression_tests[$si][$ri]{'level'} = SERVER_HEADER_TEST;
+
+                } elsif ($token eq 'trusted cgi request') {
+
+                    l(LL_FILE_LOADING, "CGI URL to test in a dumb way: " . $value);
+                    $regression_tests[$si][$ri]{'type'} = TRUSTED_CGI_REQUEST;
+                    # Implicit default
+                    $regression_tests[$si][$ri]{'expected-status-code'} = 200;
+                    $regression_tests[$si][$ri]{'level'} = TRUSTED_CGI_REQUEST;
+
+                } elsif ($token eq 'fetch test') {
+
+                    l(LL_FILE_LOADING, "URL to test in a dumb way: " . $value);
+                    $regression_tests[$si][$ri]{'type'} = DUMB_FETCH_TEST;
+                    # Implicit default
+                    $regression_tests[$si][$ri]{'expected-status-code'} = 200;
+                    $regression_tests[$si][$ri]{'level'} = DUMB_FETCH_TEST;
+
+                } elsif ($token eq 'method test') {
+
+                    l(LL_FILE_LOADING, "Method to test: " . $value);
+                    $regression_tests[$si][$ri]{'type'} = METHOD_TEST;
+                    # Implicit default
+                    $regression_tests[$si][$ri]{'expected-status-code'} = 200;
+                    $regression_tests[$si][$ri]{'level'} = METHOD_TEST;
+
+                } else {
+
+                    die "Incomplete '" . $token . "' support detected."; 
+
+                }
+
+                check_for_forbidden_characters($value);
+
+                $regression_tests[$si][$ri]{'data'} = $value;
+
+                # For function that only get passed single tests
+                $regression_tests[$si][$ri]{'section-id'} = $si;
+                $regression_tests[$si][$ri]{'regression-test-id'} = $ri;
+                $regression_tests[$si][$ri]{'file'} = $actionfile;
+
+            }
+            
+            if ($si == -1 || $ri == -1) {
+                # No beginning of a test detected yet,
+                # so we don't care about any other test
+                # attributes.
+                next;
+            }
+
+            if ($token eq 'expect header') {
+
+                l(LL_FILE_LOADING, "Detected expectation: " . $value);
+                $regression_tests[$si][$ri]{'expect-header'} = $value;
+
+            } elsif ($token eq 'tag') {
+                
+                next if ($ri == -1);
+
+                my $tag = parse_tag($value);
+
+                # We already checked in parse_tag() after filtering
+                $no_checks = 1;
+
+                l(LL_FILE_LOADING, "Detected TAG: " . $tag);
+
+                # Save tag for all tests in this section
+                do {
+                    $regression_tests[$si][$ri]{'tag'} = $tag; 
+                } while ($ri-- > 0);
+
+                $si++;
+                $ri = -1;
+
+            } elsif ($token eq 'ignore' && $value =~ /Yes/i) {
+
+                l(LL_FILE_LOADING, "Ignoring section: " . test_content_as_string($regression_tests[$si][$ri]));
+                $regression_tests[$si][$ri]{'ignore'} = 1;
+                $ignored++;
+
+            } elsif ($token eq 'expect status code') {
+
+                l(LL_FILE_LOADING, "Expecting status code: " . $value);
+                $regression_tests[$si][$ri]{'expected-status-code'} = $value;
+
+            } elsif ($token eq 'level') { # XXX: stupid name
+
+                $value =~ s@(\d+).*@$1@;
+                l(LL_FILE_LOADING, "Level: " . $value);
+                $regression_tests[$si][$ri]{'level'} = $value;
+
+            } elsif ($token eq 'method') {
+
+                l(LL_FILE_LOADING, "Method: " . $value);
+                $regression_tests[$si][$ri]{'method'} = $value;
+            } else {
+                # We don't use it, so we don't need
+                $no_checks = 1;
+            }
+            # XXX: Neccessary?
+            check_for_forbidden_characters($value) unless $no_checks;
+            check_for_forbidden_characters($token);
+        }
+    }
+
+    l(LL_FILE_LOADING, "Done loading " . $number_of_regression_tests . " regression tests." 
+      . " Of which " . $ignored. " will be ignored)\n");
+}
+
+############################################################################
+#
+# Regression test executing functions
+#
+############################################################################
+
+sub execute_regression_tests () {
+
+    our @regression_tests;
+    my $loops = get_cli_option('loops');
+    my $all_tests    = 0;
+    my $all_failures = 0;
+    my $all_successes = 0;
+
+    unless (@regression_tests) {
+
+        l(LL_STATUS, "No regression tests found.");
+        return;
+    }
+
+    l(LL_STATUS, "Executing regression tests ...");
+
+    while ($loops-- > 0) {
+
+        my $successes = 0;
+        my $tests = 0;
+        my $failures;
+        my $skipped = 0;
+        my $test_number = 0;
+
+        for my $s (0 .. @regression_tests - 1) {
+
+            my $r = 0;
+
+            while (defined $regression_tests[$s][$r]) {
+
+                die "Section id mismatch" if ($s != $regression_tests[$s][$r]{'section-id'});
+                die "Regression test id mismatch" if ($r != $regression_tests[$s][$r]{'regression-test-id'});
+
+                if ($regression_tests[$s][$r]{'ignore'}
+                    or level_is_unacceptable($regression_tests[$s][$r]{'level'})
+                    or test_number_is_unacceptable($test_number)) {
+
+                    $skipped++;
+
+                } else {
+
+                    my $result = execute_regression_test($regression_tests[$s][$r]);
+
+                    log_result($regression_tests[$s][$r], $result, $tests);
+
+                    $successes += $result;
+                    $tests++;
+                }
+                $r++;
+                $test_number++;
+            }
+        }
+        $failures = $tests - $successes;
+
+        log_message("Executed " . $tests . " regression tests. " .
+            'Skipped ' . $skipped . '. ' . 
+            $successes . " successes, " . $failures . " failures.");
+
+        $all_tests    += $tests;
+        $all_failures += $failures;
+        $all_successes += $successes;
+
+    }
+
+
+    if (get_cli_option('loops') > 1) {
+        log_message("Total: Executed " . $all_tests . " regression tests. " .
+            $all_successes . " successes, " . $all_failures . " failures.");
+    }
+}
+
+sub level_is_unacceptable ($) {
+    my $level = shift;
+    return ((cli_option_is_set('level') and get_cli_option('level') != $level)
+            or ($level < get_cli_option('min-level'))
+            or ($level > get_cli_option('max-level'))
+            );
+}
+
+sub test_number_is_unacceptable ($) {
+    my $test_number = shift;
+    return (cli_option_is_set('test-number')
+            and get_cli_option('test-number') != $test_number)
+}
+
+
+# XXX: somewhat misleading name
+sub execute_regression_test ($) {
+
+    my $test_ref = shift;
+    my %test = %{$test_ref};
+    my $result = 0;
+
+    if ($test{'type'} == CLIENT_HEADER_TEST) {
+
+        $result = execute_client_header_regression_test($test_ref);
+
+    } elsif ($test{'type'} == SERVER_HEADER_TEST) {
+
+        $result = execute_server_header_regression_test($test_ref);
+
+    } elsif ($test{'type'} == DUMB_FETCH_TEST
+          or $test{'type'} == TRUSTED_CGI_REQUEST) {
+
+        $result = execute_dumb_fetch_test($test_ref);
+
+    } elsif ($test{'type'} == METHOD_TEST) {
+
+        $result = execute_method_test($test_ref);
+
+    } else {
+
+        die "Unsuported test type detected: " . $test{'type'};
+
+    }
+
+
+    return $result;
+}
+
+sub execute_method_test ($) {
+
+    my $test_ref = shift;
+    my %test = %{$test_ref};
+    my $buffer_ref;
+    my $result = 0;
+    my $status_code;
+    my $method = $test{'data'};
+
+    my $curl_parameters = '';
+    my $expected_status_code = $test{'expected-status-code'};
+
+    if ($method =~ /HEAD/i) {
+
+        $curl_parameters .= '--head ';
+
+    } else {
+
+        $curl_parameters .= '-X ' . $method . ' ';
+    }
+
+    $curl_parameters .= PRIVOXY_CGI_URL;
+
+    $buffer_ref = get_page_with_curl($curl_parameters);
+    $status_code = get_status_code($buffer_ref);
+
+    $result = check_status_code_result($status_code, $expected_status_code);
+
+    return $result;
+}
+
+
+sub execute_dumb_fetch_test ($) {
+
+    my $test_ref = shift;
+    my %test = %{$test_ref};
+    my $buffer_ref;
+    my $result = 0;
+    my $status_code;
+
+    my $curl_parameters = '';
+    my $expected_status_code = $test{'expected-status-code'};
+
+    if (defined $test{method}) {
+        $curl_parameters .= '-X ' . $test{method} . ' ';
+    }
+    if ($test{type} == TRUSTED_CGI_REQUEST) {
+        $curl_parameters .= '--referer ' . PRIVOXY_CGI_URL . ' ';
+    }
+
+    $curl_parameters .= $test{'data'};
+
+    $buffer_ref = get_page_with_curl($curl_parameters);
+    $status_code = get_status_code($buffer_ref);
+
+    $result = check_status_code_result($status_code, $expected_status_code);
+
+    return $result;
+}
+
+sub check_status_code_result ($$) {
+
+    my $status_code = shift;
+    my $expected_status_code = shift;
+    my $result = 0;
+
+    if ($expected_status_code == $status_code) {
+
+        $result = 1;
+        l(LL_VERBOSE_SUCCESS,
+          "Yay. We expected status code " . $expected_status_code . ", and received: " . $status_code . '.');
+
+    } elsif (cli_option_is_set('fuzzer-feeding') and $status_code == 123) {
+
+        l(LL_VERBOSE_FAILURE,
+          "Oh well. Status code lost while fuzzing. Can't check if it was " . $expected_status_code . '.');
+
+    } else {
+
+        l(LL_VERBOSE_FAILURE,
+          "Ooops. We expected status code " . $expected_status_code . ", but received: " . $status_code . '.');
+
+    }
+    
+    return $result;
+}
+
+sub execute_client_header_regression_test ($) {
+
+    my $test_ref = shift;
+    my $buffer_ref;
+    my $header;
+    my $result = 0;
+
+    $buffer_ref = get_show_request_with_curl($test_ref);
+
+    $header = get_header($buffer_ref, $test_ref);
+    $result = check_header_result($test_ref, $header);
+
+    return $result;
+}
+
+sub execute_server_header_regression_test ($) {
+
+    my $test_ref = shift;
+    my $buffer_ref;
+    my $header;
+    my $result = 0;
+
+    $buffer_ref = get_head_with_curl($test_ref);
+
+    $header = get_server_header($buffer_ref, $test_ref);
+    $result = check_header_result($test_ref, $header);
+
+    return $result;
+}
+
+
+sub interpret_result ($) {
+    my $success = shift;
+    return $success ? "Success" : "Failure";
+}
+
+sub check_header_result ($$) {
+
+    my $test_ref = shift;
+    my $header = shift;
+
+    my %test = %{$test_ref};
+    my $expect_header = $test{'expect-header'};
+    my $success = 0;
+
+    $header =~ s@   @ @g if defined($header);
+
+    if ($expect_header eq 'NO CHANGE') {
+
+        if (defined($header) and $header eq $test{'data'}) {
+
+            $success = 1;
+
+        } else {
+
+            $header //= "REMOVAL";
+            l(LL_VERBOSE_FAILURE,
+              "Ooops. Got: " . $header . " while expecting: " . $expect_header);
+        }
+
+    } elsif ($expect_header eq 'REMOVAL') {
+
+        if (defined($header) and $header eq $test{'data'}) {
+
+            l(LL_VERBOSE_FAILURE,
+              "Ooops. Expected removal but: " . $header . " is still there.");
+
+        } else {
+
+            # XXX: Use more reliable check here and make sure
+            # the header has a different name.
+            $success = 1;
+
+        }
+
+    } elsif ($expect_header eq 'SOME CHANGE') {
+
+        if (defined($header) and not $header eq $test{'data'}) {
+
+            $success = 1;
+
+        } else {
+
+            $header //= "REMOVAL";
+            l(LL_VERBOSE_FAILURE,
+              "Ooops. Got: " . $header . " while expecting: SOME CHANGE");
+        }
+
+
+    } else {
+
+        if (defined($header) and $header eq $expect_header) {
+
+            $success = 1;
+
+        } else {
+
+            $header //= "'No matching header'"; # XXX: No header detected to be precise
+            l(LL_VERBOSE_FAILURE,
+              "Ooops. Got: " . $header . " while expecting: " . $expect_header);
+        }
+    }
+    return $success;
+}
+
+
+sub get_header_name ($) {
+
+    my $header = shift;
+
+    $header =~ s@(.*?: ).*@$1@;
+
+    return $header;
+}
+
+sub get_header ($$) {
+
+    our $filtered_request = '';
+
+    my $buffer_ref = shift;
+    my $test_ref = shift;
+
+    my %test = %{$test_ref};
+    my @buffer = @{$buffer_ref};
+
+    my $expect_header = $test{'expect-header'};
+
+    my $line;
+    my $processed_request_reached = 0;
+    my $read_header = 0;
+    my $processed_request = '';
+    my $header;
+    my $header_to_get;
+
+    if ($expect_header eq 'REMOVAL'
+     or $expect_header eq 'NO CHANGE'
+     or  $expect_header eq 'SOME CHANGE') {
+
+        $expect_header = $test{'data'};
+
+    }
+
+    $header_to_get = get_header_name($expect_header);
+
+    foreach (@buffer) {
+
+        # Skip everything before the Processed request
+        if (/Processed Request/) {
+            $processed_request_reached = 1;
+            next;
+        }
+        next unless $processed_request_reached;
+
+        # End loop after the Processed request
+        last if (/<\/pre>/);
+
+        # Ditch tags and leading/trailing white space.
+        s@^\s*<.*?>@@g;
+        s@\s*$@@g;
+
+        $filtered_request .=  "\n" . $_;
+         
+        if (/^$header_to_get/) {
+            $read_header = 1;
+            $header = $_;
+            last;
+        }
+    }
+
+    return $header;
+}
+
+sub get_server_header ($$) {
+
+    my $buffer_ref = shift;
+    my $test_ref = shift;
+
+    my %test = %{$test_ref};
+    my @buffer = @{$buffer_ref};
+
+    my $expect_header = $test{'expect-header'};
+    my $header;
+    my $header_to_get;
+
+    if ($expect_header eq 'REMOVAL'
+     or $expect_header eq 'NO CHANGE'
+     or  $expect_header eq 'SOME CHANGE') {
+
+        $expect_header = $test{'data'};
+
+    }
+
+    $header_to_get = get_header_name($expect_header);
+
+    foreach (@buffer) {
+
+        # XXX: shoul probably verify that the request
+        # was actually answered by Fellatio.
+        if (/^$header_to_get/) {
+            $header = $_;
+            $header =~ s@\s*$@@g;
+            last;
+        }
+    }
+
+    return $header;
+}
+
+sub get_header_to_check ($) {
+
+    # No longer in use but not removed yet.
+
+    my $buffer_ref = shift;
+    my $header;
+    my @buffer = @{$buffer_ref}; 
+    my $line;
+    my $processed_request_reached = 0;
+    my $read_header = 0;
+    my $processed_request = '';
+
+    l(LL_ERROR, "You are not supposed to use get_header_to_()!");
+
+    foreach (@buffer) {
+
+        # Skip everything before the Processed request
+        if (/Processed Request/) {
+            $processed_request_reached = 1;
+            next;
+        }
+        next unless $processed_request_reached;
+
+        # End loop after the Processed request
+        last if (/<\/pre>/);
+
+        # Ditch tags and leading/trailing white space.
+        s@^\s*<.*?>@@g;
+        s@\s*$@@g;
+
+        $processed_request .= $_;
+         
+        if (/^X-Privoxy-Regression-Test/) {
+            $read_header = 1;
+            next;
+        }
+
+        if ($read_header) {
+            $header = $_;
+            $read_header = 0;
+        }
+
+    }
+
+    return $header;
+}
+
+sub get_status_code ($) {
+
+    my $buffer_ref = shift;
+    my @buffer = @{$buffer_ref}; 
+
+    foreach (@buffer) {
+
+        if (/^HTTP\/\d\.\d (\d{3})/) {
+
+            return $1;
+
+        } else {
+
+            return '123' if cli_option_is_set('fuzzer-feeding');
+            chomp;
+            l(LL_ERROR, 'Unexpected buffer line: "' . $_ . '"');
+        }
+    }
+}
+
+sub get_test_keys () {
+    return ('tag', 'data', 'expect-header', 'ignore');
+}
+
+# XXX: incomplete
+sub test_content_as_string ($) {
+
+    my $test_ref = shift;
+    my %test = %{$test_ref};
+
+    my $s = "\n\t";
+
+    foreach my $key (get_test_keys()) {
+        #$test{$key} = $test{$key} // 'undefined';
+        $test{$key} = 'Not set' unless (defined $test{$key});
+    }
+
+    $s .= 'Tag: ' . $test{'tag'};
+    $s .= "\n\t";
+    $s .= 'Set header: ' . $test{'data'}; # XXX: adjust for other test types
+    $s .= "\n\t";
+    $s .= 'Expected header: ' . $test{'expect-header'};
+    $s .= "\n\t";
+    $s .= 'Ignore: ' . $test{'ignore'};
+
+    return $s;
+}
+
+############################################################################
+#
+# HTTP fetch functions
+#
+############################################################################
+
+sub check_for_curl () {
+    my $curl = CURL;
+    l(LL_ERROR, "No curl found.") unless (`which $curl`);
+}
+
+sub get_cgi_page_or_else ($) {
+
+    my $cgi_url = shift;
+    my $content_ref = get_page_with_curl($cgi_url);
+    my $status_code = get_status_code($content_ref);
+
+    if (200 != $status_code) {
+
+        my $log_message = "Failed to fetch Privoxy CGI Page. " .
+                          "Received status code ". $status_code .
+                          " while only 200 is acceptable.";
+
+        if (cli_option_is_set('fuzzer-feeding')) {
+
+            $log_message .= " Ignored due to fuzzer feeding.";
+            l(LL_SOFT_ERROR, $log_message)
+
+        } else {
+
+            l(LL_ERROR, $log_message);
+
+        }
+    }
+    
+    return $content_ref;
+}
+
+sub get_show_request_with_curl ($) {
+
+    our $privoxy_cgi_url;
+    my $test_ref = shift;
+    my %test = %{$test_ref};
+
+    my $curl_parameters = ' ';
+
+    # Enable the action to test
+    $curl_parameters .= '-H \'X-Privoxy-Control: ' . $test{'tag'} . '\' ';
+    # The header to filter
+    $curl_parameters .= '-H \'' . $test{'data'} . '\' ';
+
+    $curl_parameters .= ' ';
+    $curl_parameters .= $privoxy_cgi_url;
+    $curl_parameters .= 'show-request';
+
+    return get_cgi_page_or_else($curl_parameters);
+}
+
+
+sub get_head_with_curl ($) {
+
+    our $fellatio_url = FELLATIO_URL;
+    my $test_ref = shift;
+    my %test = %{$test_ref};
+
+    my $curl_parameters = ' ';
+
+    # Enable the action to test
+    $curl_parameters .= '-H \'X-Privoxy-Control: ' . $test{'tag'} . '\' ';
+    # The header to filter
+    $curl_parameters .= '-H \'X-Gimme-Head-With: ' . $test{'data'} . '\' ';
+    $curl_parameters .= '--head ';
+
+    $curl_parameters .= ' ';
+    $curl_parameters .= $fellatio_url;
+
+    return get_page_with_curl($curl_parameters);
+}
+
+
+sub get_page_with_curl ($) {
+
+    my $parameters = shift;
+    my @buffer;
+    my $curl_line = CURL;
+    my $retries_left = get_cli_option('retries') + 1;
+    my $failure_reason;
+
+    if (cli_option_is_set('privoxy-address')) {
+        $curl_line .= ' --proxy ' . get_cli_option('privoxy-address');
+    }
+
+    # Let Privoxy emit two log messages less.
+    $curl_line .= ' -H \'Proxy-Connection:\' ' unless $parameters =~ /Proxy-Connection:/;
+    $curl_line .= ' -H \'Connection: close\' ' unless $parameters =~ /Connection:/;
+    # We don't care about fetch statistic.
+    $curl_line .= " -s ";
+    # We do care about the failure reason if any.
+    $curl_line .= " -S ";
+    # We want to see the HTTP status code
+    $curl_line .= " --include ";
+    # We want to advertise ourselves
+    $curl_line .= " --user-agent '" . PRT_VERSION . "' ";
+    # We aren't too patient
+    $curl_line .= " --max-time '" . get_cli_option('max-time') . "' ";
+
+    $curl_line .= $parameters;
+    # XXX: still necessary?
+    $curl_line .= ' 2>&1';
+
+    l(LL_PAGE_FETCHING, "Executing: " . $curl_line);
+
+    do {
+        @buffer = `$curl_line`;
+
+        if ($?) {
+            $failure_reason = array_as_string(\@buffer);
+            chomp $failure_reason;
+            l(LL_SOFT_ERROR, "Fetch failure: '" . $failure_reason . $! ."'");
+        }
+    } while ($? && --$retries_left);
+
+    unless ($retries_left) {
+        l(LL_ERROR,
+          "Running curl failed " . get_cli_option('retries') .
+          " times in a row. Last error: '" . $failure_reason . "'.");
+    }
+
+    return \@buffer;
+}
+
+
+############################################################################
+#
+# Log functions
+#
+############################################################################
+
+sub array_as_string ($) {
+    my $array_ref = shift;
+    my $string = '';
+
+    foreach (@{$array_ref}) {
+        $string .= $_;
+    }
+
+    return $string;
+}
+
+
+sub show_test ($) {
+    my $test_ref = shift;
+    log_message('Test is:' . test_content_as_string($test_ref));
+}
+
+# Conditional log
+sub l ($$) {
+    our $log_level;
+    my $this_level = shift;
+    my $message = shift;
+
+    return unless ($log_level & $this_level);
+
+    if (LL_ERROR & $this_level) {
+        $message = 'Oh noes. ' . $message . ' Fatal error. Exiting.';
+    }
+
+    log_message($message);
+
+    if (LL_ERROR & $this_level) {
+        exit;
+    }
+}
+
+sub log_message ($) {
+
+    my $message = shift;
+
+    our $logfile;
+    our $no_logging;
+    our $leading_log_date;
+    our $leading_log_time;
+
+    my $time_stamp = '';
+    my ( $sec, $min, $hour, $mday, $mon, $year, $wday, $yday, $isdst ) = localtime time;
+
+    if ($leading_log_date || $leading_log_time) {
+
+        if ($leading_log_date) {
+            $year += 1900;
+            $mon  += 1;
+            $time_stamp = sprintf("%i/%.2i/%.2i", $year, $mon, $mday);
+        }
+
+        if ($leading_log_time) {
+            $time_stamp .= ' ' if $leading_log_date;
+            $time_stamp.= sprintf("%.2i:%.2i:%.2i", $hour, $min, $sec);
+        }
+        
+        $message = $time_stamp . ": " . $message;
+    }
+
+
+    printf(STDERR "%s\n", $message);
+
+}
+
+sub log_result ($$) {
+
+    our $verbose_test_description;
+    our $filtered_request;
+
+    my $test_ref = shift;
+    my $result = shift;
+    my $number = shift;
+
+    my %test = %{$test_ref};
+    my $message = '';
+
+    $message .= interpret_result($result);
+    $message .= " for test ";
+    $message .= $number;
+    $message .= '/';
+    $message .= $test{'section-id'};
+    $message .= '/';
+    $message .= $test{'regression-test-id'};
+    $message .= '.';
+
+    if ($verbose_test_description) {
+
+        if ($test{'type'} == CLIENT_HEADER_TEST) {
+
+            $message .= ' Header ';
+            $message .= quote($test{'data'});
+            $message .= ' and tag ';
+            $message .= quote($test{'tag'});
+
+        } elsif ($test{'type'} == SERVER_HEADER_TEST) {
+
+            $message .= ' Request Header ';
+            $message .= quote($test{'data'});
+            $message .= ' and tag ';
+            $message .= quote($test{'tag'});
+
+        } elsif ($test{'type'} == DUMB_FETCH_TEST) {
+
+            $message .= ' URL ';
+            $message .= quote($test{'data'});
+            $message .= ' and expected status code ';
+            $message .= quote($test{'expected-status-code'});
+
+        } elsif ($test{'type'} == TRUSTED_CGI_REQUEST) {
+
+            $message .= ' CGI URL ';
+            $message .= quote($test{'data'});
+            $message .= ' and expected status code ';
+            $message .= quote($test{'expected-status-code'});
+
+        } elsif ($test{'type'} == METHOD_TEST) {
+
+            $message .= ' HTTP method ';
+            $message .= quote($test{'data'});
+            $message .= ' and expected status code ';
+            $message .= quote($test{'expected-status-code'});
+
+        } else {
+
+            die "Incomplete support for test type " . $test{'type'} .  " detected.";
+
+        }
+    }
+
+    log_message($message) unless ($result && cli_option_is_set('silent'));
+}
+
+sub quote ($) {
+    my $s = shift;
+    return '\'' . $s . '\'';
+}
+
+sub print_version () {
+    printf PRT_VERSION . "\n" . 'Copyright (C) 2007-2008 Fabian Keil <fk@fabiankeil.de>' . "\n";
+}
+
+sub help () {
+
+    our %cli_options;
+
+    print_version();
+
+    print << "    EOF"
+
+Options and their default values if they have any:
+    [--debug $cli_options{'debug'}]
+    [--fuzzer-feeding]
+    [--help]
+    [--level]
+    [--loops $cli_options{'loops'}]
+    [--max-level $cli_options{'max-level'}]
+    [--max-time $cli_options{'max-time'}]
+    [--min-level $cli_options{'min-level'}]
+    [--privoxy-address]
+    [--retries $cli_options{'retries'}]
+    [--silent]
+    [--version]
+see "perldoc $0" for more information
+    EOF
+    ;
+    exit(0);
+}
+
+sub init_cli_options () {
+
+    our %cli_options;
+    our $log_level;
+
+    $cli_options{'min-level'} = CLI_MIN_LEVEL;
+    $cli_options{'max-level'} = CLI_MAX_LEVEL;
+    $cli_options{'debug'}  = $log_level;
+    $cli_options{'loops'}  = CLI_LOOPS;
+    $cli_options{'max-time'}  = CLI_MAX_TIME;
+    $cli_options{'retries'}  = CLI_RETRIES;
+}
+
+sub parse_cli_options () {
+
+    our %cli_options;
+    our $log_level;
+
+    init_cli_options();
+
+    GetOptions (
+                'debug=s' => \$cli_options{'debug'},
+                'help'     => sub { help },
+                'silent' => \$cli_options{'silent'},
+                'min-level=s' => \$cli_options{'min-level'},
+                'max-level=s' => \$cli_options{'max-level'},
+                'privoxy-address=s' => \$cli_options{'privoxy-address'},
+                'level=s' => \$cli_options{'level'},
+                'loops=s' => \$cli_options{'loops'},
+                'test-number=s' => \$cli_options{'test-number'},
+                'fuzzer-feeding' => \$cli_options{'fuzzer-feeding'},
+                'retries=s' => \$cli_options{'retries'},
+                'max-time=s' => \$cli_options{'max-time'},
+                'version'  => sub { print_version && exit(0) }
+    );
+    $log_level |= $cli_options{'debug'};
+}
+
+sub cli_option_is_set ($) {
+
+    our %cli_options;
+    my $cli_option = shift;
+
+    return defined $cli_options{$cli_option};
+}
+
+
+sub get_cli_option ($) {
+
+    our %cli_options;
+    my $cli_option = shift;
+
+    die "Unknown CLI option: $cli_option" unless defined $cli_options{$cli_option};
+
+    return $cli_options{$cli_option};
+}
+
+
+sub main () {
+
+    init_our_variables();
+    parse_cli_options();
+    check_for_curl();
+    load_regressions_tests();
+    execute_regression_tests();
+}
+
+main();
+
+=head1 NAME
+
+B<privoxy-regression-test> - A regression test "framework" for Privoxy.
+
+=head1 SYNOPSIS
+
+B<privoxy-regression-test> [B<--debug bitmask>] [B<--fuzzer-feeding>] [B<--help>]
+[B<--level level>] [B<--loops count>] [B<--max-level max-level>]
+[B<--max-time max-time>] [B<--min-level min-level>] B<--privoxy-address proxy-address>
+[B<--retries retries>] [B<--silent>] [B<--version>]
+
+=head1 DESCRIPTION
+
+Privoxy-Regression-Test is supposed to one day become
+a regression test suite for Privoxy. It's not quite there
+yet, however, and can currently only test client header
+actions and check the returned status code for requests
+to arbitrary URLs.
+
+Client header actions are tested by requesting
+B<http://config.privoxy.org/show-request> and checking whether
+or not Privoxy modified the original request as expected.
+
+The original request contains both the header the action-to-be-tested
+acts upon and an additional tagger-triggering header that enables
+the action to test.
+
+=head1 CONFIGURATION FILE SYNTAX
+
+Privoxy-Regression-Test's configuration is embedded in
+Privoxy action files and loaded through Privoxy's web interface.
+
+It makes testing a Privoxy version running on a remote system easier
+and should prevent you from updating your tests without updating Privoxy's
+configuration accordingly.
+
+A client-header-action test section looks like this:
+
+    # Set Header    = Referer: http://www.example.org.zwiebelsuppe.exit/
+    # Expect Header = Referer: http://www.example.org/
+    {+client-header-filter{hide-tor-exit-notation} -hide-referer}
+    TAG:^client-header-filter\{hide-tor-exit-notation\}$
+
+The example above causes Privoxy-Regression-Test to set
+the header B<Referer: http://www.example.org.zwiebelsuppe.exit/>
+and to expect it to be modified to
+B<Referer: http://www.example.org/>.
+
+When testing this section, Privoxy-Regression-Test will set the header
+B<X-Privoxy-Control: client-header-filter{hide-tor-exit-notation}>
+causing the B<privoxy-control> tagger to create the tag
+B<client-header-filter{hide-tor-exit-notation}> which will finally
+cause Privoxy to enable the action section.
+
+Note that the actions itself are only used by Privoxy,
+Privoxy-Regression-Test ignores them and will be happy
+as long as the expectations are satisfied.
+
+A fetch test looks like this:
+
+    # Fetch Test = http://p.p/user-manual
+    # Expect Status Code = 302
+
+It tells Privoxy-Regression-Test to request B<http://p.p/user-manual>
+and to expect a response with the HTTP status code B<302>. Obviously that's
+not a very thorough test and mainly useful to get some code coverage
+for Valgrind or to verify that the templates are installed correctly.
+
+If you want to test CGI pages that require a trusted
+referer, you can use:
+
+    # Trusted CGI Request =  http://p.p/edit-actions
+
+It works like ordinary fetch tests, but sets the referer
+header to a trusted value.
+
+If no explicit status code expectation is set, B<200> is used.
+
+Additionally all tests have test levels to let the user
+control which ones to execute (see I<OPTIONS> below). 
+Test levels are either set with the B<Level> directive,
+or implicitly through the test type.
+
+Fetch tests default to level 6, tests for trusted
+CGI requests to level 3 and client-header-action tests
+to level 1.
+
+=head1 OPTIONS
+
+B<--debug bitmask> Add the bitmask provided as integer
+to the debug settings.
+
+B<--fuzzer-feeding> Ignore some errors that would otherwise
+cause Privoxy-Regression-Test to abort the test because
+they shouldn't happen in normal operation. This option is
+intended to be used if Privoxy-Regression-Test is only
+used to feed a fuzzer in which case there's a high chance
+that Privoxy gets an invalid request and returns an error
+message.
+
+B<--help> Shows available command line options.
+
+B<--level level> Only execute tests with the specified B<level>. 
+
+B<--loop count> Loop through the regression tests B<count> times. 
+Useful to feed a fuzzer, or when doing stress tests with
+several Privoxy-Regression-Test instances running at the same
+time.
+
+B<--max-level max-level> Only execute tests with a B<level>
+below or equal to the numerical B<max-level>.
+
+B<--max-time max-time> Give Privoxy B<max-time> seconds
+to return data. Increasing the default may make sense when
+Privoxy is run through Valgrind, decreasing the default may
+make sense when Privoxy-Regression-Test is used to feed
+a fuzzer.
+
+B<--min-level min-level> Only execute tests with a B<level>
+above or equal to the numerical B<min-level>.
+
+B<--privoxy-address proxy-address> Privoxy's listening address.
+If it's not set, the value of the environment variable http_proxy
+will be used. B<proxy-address> has to be specified in http_proxy
+syntax.
+
+B<--retries retries> Retry B<retries> times.
+
+B<--silent> Don't log succesful test runs.
+
+B<--version> Print version and exit.
+
+The second dash is optional, options can be shortened,
+as long as there are no ambiguities.
+
+=head1 PRIVOXY CONFIGURATION
+
+Privoxy-Regression-Test is shipped with B<regression-tests.action>
+which aims to test all official client-header modifying actions
+and can be used to verify that the templates and the user manual
+files are installed correctly.
+
+To use it, it has to be copied in Privoxy's configuration
+directory, and afterwards referenced in Privoxy's configuration
+file with the line:
+
+    actionsfile regression-tests.action
+
+In general, its tests are supposed to work without changing
+any other action files, unless you already added lots of
+taggers yourself. If you are using taggers that cause problems,
+you might have to temporary disable them for Privoxy's CGI pages.
+
+Some of the regression tests rely on Privoxy features that
+may be disabled in your configuration. Tests with a level below
+7 are supposed to work with all Privoxy configurations (provided
+you didn't build with FEATURE_GRACEFUL_TERMINATION).
+
+Tests with level 9 require Privoxy to deliver the User Manual,
+tests with level 12 require the CGI editor to be enabled.
+
+=head1 CAVEATS
+
+Expect the configuration file syntax to change with future releases.
+
+=head1 LIMITATIONS
+
+As Privoxy's B<show-request> page only shows client headers,
+Privoxy-Regression-Test can't use it to test Privoxy actions
+that modify server headers.
+
+As Privoxy-Regression-Test relies on Privoxy's tag feature to
+control the actions to test, it currently only works with
+Privoxy 3.0.7 or later.
+
+At the moment Privoxy-Regression-Test fetches Privoxy's
+configuration page through I<curl>(1), therefore you have to
+have I<curl> installed, otherwise you won't be able to run
+Privoxy-Regression-Test in a meaningful way.
+
+=head1 SEE ALSO
+
+privoxy(1) curl(1)
+
+=head1 AUTHOR
+
+Fabian Keil <fk@fabiankeil.de>
+
+=cut