Skip to content

Commit

Permalink
Add development documentation and framework for code coverage measure…
Browse files Browse the repository at this point in the history
…ment
  • Loading branch information
grooverdan committed Mar 10, 2013
1 parent 00ad4d5 commit 3665e6d
Show file tree
Hide file tree
Showing 9 changed files with 144 additions and 27 deletions.
4 changes: 4 additions & 0 deletions .coveragerc
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@

[run]
branch = True
omit = /usr*
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
*~
build
dist
*.pyc
htmlcov
.coverage
122 changes: 115 additions & 7 deletions DEVELOP
Original file line number Diff line number Diff line change
Expand Up @@ -24,14 +24,96 @@ Request feature. You can find more details on the Fail2Ban wiki
Testing
=======

Existing tests can be run by executing `fail2ban-testcases`.
Existing tests can be run by executing `fail2ban-testcases`. This has options
like --log-level that will probably be useful. `fail2ban-testcases --help` for
full options.

Test cases should cover all usual cases, all exception cases and all inside
/ outside boundary conditions.

Test cases should cover all branches. The coverage tool will help identify
missing branches. Also see http://nedbatchelder.com/code/coverage/branch.html
for more details.

Install the package python-coverage to visualise your test coverage. Run the
following:

coverage run fail2ban-testcases
coverage html

Then look at htmlcov/index.html and see how much coverage your test cases
exert over the codebase. Full coverage is a good thing however it may not be
complete. Try to ensure tests cover as many independant paths through the
code.

Manual Execution. To run in a development environment do:

./fail2ban-client -c config/ -s /tmp/f2b.sock -i start

some quick commands:

status
add test pyinotify
status test
set test addaction iptables
set test actionban iptables echo <ip> <cidr> >> /tmp/ban
set test actionunban iptables echo <ip> <cidr> >> /tmp/unban
get test actionban iptables
get test actionunban iptables
set test banip 192.168.2.2
status test


Documentation about creating tests (when tests are required and some guidelines
for creating good tests) will be added soon.

Coding Standards
================
Coming Soon.
================

Style
-----

Please use tabs for now. Keep to 80 columns, at least for readable text.

Tests
-----

Add tests. They should test all the code you add in a meaning way.

Coverage
--------

Test coverage should always increase as you add code.

You may use "# pragma: no cover" in the code for branches of code that support
older versions on python. For all other uses of "pragma: no cover" or
"pragma: no branch" document the reason why its not covered. "I haven't written
a test case" isn't a sufficient reason.

Documentation
-------------

Ensure this documentation is up to date after changes. Also ensure that the man
pages still are accurage. Ensure that there is sufficient documentation for
your new features to be used.

Bugs
----

Remove them and don't add any more.

Git
---

Use the following tags in your commit messages:

'ENH:' for enhancements
'BF:' for bug fixes
'DOC:' for documenation fixes

Adding Actions
--------------

If you add an action.d/*.conf file also add a example in config/jail.conf
with enabled=false and maxretry=5 for ssh.


Design
Expand Down Expand Up @@ -127,12 +209,14 @@ FileContainer
.__pos
Keeps the position pointer


dnsutils.py
~~~~~~~~~~~

DNSUtils

Utility class for DNS and IP handling

RF-Note: convert to functions within a separate submodule


filter*.py
~~~~~~~~~~
Expand All @@ -156,3 +240,27 @@ action.py
~~~~~~~~~

Takes care about executing start/check/ban/unban/stop commands


Releasing
=========

Ensure the version is correct in ./common/version.py

# update man pages
(cd man ; ./generate-man )

git commit -m 'update man pages for release' man/*

python setup.py check
python setup.py sdist
python setup.py bdist_rpm
python setup.py upload

Run the following and update the wiki with output:

python -c 'import common.protocol; common.protocol.printWiki()'

email users and development list of release

TODO notifing distributors etc.
2 changes: 2 additions & 0 deletions MANIFEST
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ ChangeLog
TODO
THANKS
COPYING
DEVELOP
doc/run-rootless.txt
fail2ban-client
fail2ban-server
fail2ban-testcases
Expand Down
2 changes: 1 addition & 1 deletion fail2ban-client
Original file line number Diff line number Diff line change
Expand Up @@ -418,7 +418,7 @@ class Fail2banClient:
class ServerExecutionException(Exception):
pass

if __name__ == "__main__":
if __name__ == "__main__": # pragma: no cover - can't test main
client = Fail2banClient()
# Exit with correct return value
if client.start(sys.argv):
Expand Down
22 changes: 11 additions & 11 deletions fail2ban-testcases
Original file line number Diff line number Diff line change
Expand Up @@ -77,10 +77,10 @@ verbosity = {'debug': 3,
'fatal': 0,
None: 1}[opts.log_level]

if opts.log_level is not None:
if opts.log_level is not None: # pragma: no cover
# so we had explicit settings
logSys.setLevel(getattr(logging, opts.log_level.upper()))
else:
else: # pragma: no cover
# suppress the logging but it would leave unittests' progress dots
# ticking, unless like with '-l fatal' which would be silent
# unless error occurs
Expand All @@ -89,27 +89,27 @@ else:
# Add the default logging handler
stdout = logging.StreamHandler(sys.stdout)
# Custom log format for the verbose tests runs
if verbosity > 1:
if verbosity > 1: # pragma: no cover
stdout.setFormatter(logging.Formatter(' %(asctime)-15s %(thread)s %(message)s'))
else:
else: # pragma: no cover
# just prefix with the space
stdout.setFormatter(logging.Formatter(' %(message)s'))
logSys.addHandler(stdout)

#
# Let know the version
#
if not opts.log_level or opts.log_level != 'fatal':
if not opts.log_level or opts.log_level != 'fatal': # pragma: no cover
print "Fail2ban %s test suite. Python %s. Please wait..." \
% (version, str(sys.version).replace('\n', ''))


#
# Gather the tests
#
if not len(regexps):
if not len(regexps): # pragma: no cover
tests = unittest.TestSuite()
else:
else: # pragma: no cover
import re
class FilteredTestSuite(unittest.TestSuite):
_regexps = [re.compile(r) for r in regexps]
Expand Down Expand Up @@ -159,13 +159,13 @@ filters = [FilterPoll] # always available
try:
from server.filtergamin import FilterGamin
filters.append(FilterGamin)
except Exception, e:
except Exception, e: # pragma: no cover
print "I: Skipping gamin backend testing. Got exception '%s'" % e

try:
from server.filterpyinotify import FilterPyinotify
filters.append(FilterPyinotify)
except Exception, e:
except Exception, e: # pragma: no cover
print "I: Skipping pyinotify backend testing. Got exception '%s'" % e

for Filter_ in filters:
Expand All @@ -189,13 +189,13 @@ try:

tests_results = testRunner.run(tests)

finally:
finally: # pragma: no cover
# Just for the sake of it reset the TZ
# yoh: move all this into setup/teardown methods within tests
os.environ.pop('TZ')
if old_TZ:
os.environ['TZ'] = old_TZ
time.tzset()

if not tests_results.wasSuccessful():
if not tests_results.wasSuccessful(): # pragma: no cover
sys.exit(1)
2 changes: 1 addition & 1 deletion server/asyncserver.py
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,7 @@ def start(self, sock, force):
if sys.version_info >= (2, 6): # if python 2.6 or greater...
logSys.debug("Detected Python 2.6 or greater. asyncore.loop() not using poll")
asyncore.loop(use_poll = False) # fixes the "Unexpected communication problem" issue on Python 2.6 and 3.0
else:
else: # pragma: no cover
logSys.debug("NOT Python 2.6/3.* - asyncore.loop() using poll")
asyncore.loop(use_poll = True)

Expand Down
8 changes: 4 additions & 4 deletions server/filter.py
Original file line number Diff line number Diff line change
Expand Up @@ -211,7 +211,7 @@ def getMaxRetry(self):
# file has been modified and looks for failures.
# @return True when the thread exits nicely

def run(self):
def run(self): # pragma: no cover
raise Exception("run() is abstract")

##
Expand All @@ -226,7 +226,7 @@ def addBannedIP(self, ip):
self.failManager.addFailure(FailTicket(ip, unixTime))

# Perform the banning of the IP now.
try:
try: # pragma: no branch - exception is the only way out
while True:
ticket = self.failManager.toBan()
self.jail.putFailTicket(ticket)
Expand Down Expand Up @@ -373,7 +373,7 @@ def findFailure(self, timeLine, logLine):
failList.append([ip, date])
# We matched a regex, it is enough to stop.
break
except RegexException, e:
except RegexException, e: # pragma: no cover - unsure if reachable
logSys.error(e)
return failList

Expand Down Expand Up @@ -507,7 +507,7 @@ def status(self):
try:
import hashlib
md5sum = hashlib.md5
except ImportError:
except ImportError: # pragma: no cover
# hashlib was introduced in Python 2.5. For compatibility with those
# elderly Pythons, import from md5
import md5
Expand Down
6 changes: 3 additions & 3 deletions testcases/filtertestcase.py
Original file line number Diff line number Diff line change
Expand Up @@ -99,11 +99,11 @@ def _copy_lines_between_files(fin, fout, n=None, skip=0, mode='a', terminal_line
Returns open fout
"""
if sys.version_info[:2] <= (2,4):
if sys.version_info[:2] <= (2,4): # pragma: no cover
# on old Python st_mtime is int, so we should give at least 1 sec so
# polling filter could detect the change
time.sleep(1)
if isinstance(fin, str):
if isinstance(fin, str): # pragma: no branch - only used with str in test cases
fin = open(fin, 'r')
if isinstance(fout, str):
fout = open(fout, mode)
Expand Down Expand Up @@ -353,7 +353,7 @@ def tearDown(self):
_killfile(self.file, self.name)
pass

def __str__(self):
def __str__(self): # pragma: no cover - will only show up if unexpected exception is thrown
return "MonitorFailures%s(%s)" \
% (Filter_, hasattr(self, 'name') and self.name or 'tempfile')

Expand Down

0 comments on commit 3665e6d

Please sign in to comment.