unittest framework kickoff

svn: r9301
This commit is contained in:
James G Sack 2007-11-06 03:51:21 +00:00
parent 3a9ef9d71f
commit 84a7a512f8
5 changed files with 517 additions and 0 deletions

View File

@ -1,3 +1,9 @@
2007-11-05 Jim Sack <jgsack@san.rr.com>
* src/test directory added
* src/test/{__init__,regrtest,test_init}.py added
* src/test/test/test_util_test.py added
First steps in a new unittest framework
2007-11-05 Jim Sack <jgsack@san.rr.com>
* remove 2 files from version control: config.guess and config.sub
These are generated by configure (via autogen.sh).

46
src/test/__init__.py Normal file
View File

@ -0,0 +1,46 @@
#
# Gramps - a GTK+/GNOME based genealogy program
#
# Copyright (C) 2004-2006 Donald N. Allingham
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
#
# $Id:$
"""
This package implements unittest support for GRAMPS
It includes a test-running program regrtest.py,
and various test support utility modules
(first one created being test_util.py)
Also includes tests for code in the parent (src) directory
and other tests that may be considered top-level tests
Note: tests for utility code in this directory would be in a
subdirectory also named test by convention for gramps testing.
Note: test subdirectories do not normally need to have a
package-enabling module __init__.py, but this one (src/test) does
because it contains utilities used by other test modules.
Thus, this package would allow test code like
from test import test_util
"""
# This file does not presently contain any code.
#===eof===

155
src/test/regrtest.py Executable file
View File

@ -0,0 +1,155 @@
#! /usr/bin/env python
#
# Gramps - a GTK+/GNOME based genealogy program
#
# Copyright (C) 2000-2005 Donald N. Allingham
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
#
# Original: RunAllTests.py Written by Richard Taylor
# (jgs: revised for embedded "test" subdirs as regrtest.py )
"""
Testing framework for performing a variety of unttests for GRAMPS.
"""
# TODO: review whether logging is really useful for unittest
# it does seem to work .. try -v5
import logging
import os
import sys
import unittest
from optparse import OptionParser
from test import test_util as tu
gramps_root=tu.path_append_parent()
def make_parser():
usage = "usage: %prog [options]"
parser = OptionParser(usage)
parser.add_option("-v", "--verbosity", type="int",
dest="verbose_level", default=0,
help="Level of verboseness")
parser.add_option("-p", "--performance", action="store_true",
dest="performance", default=False,
help="Run the performance tests.")
return parser
def getTestSuites(loc=gramps_root):
# in a developer's checkout, it is worth filtering-out .svn
# and we only need to look inside test subdirs
# (this might change)
# this is not so performance critical that we can't afford
# a couple of function calls to make it readable
# TODO: handle parts of a module (see unittest.py)
ldr= unittest.defaultTestLoader
test_dirname = "test"
test_suffix = "_test.py"
def test_mod(p,ds):
""" test for path p=test dir; removes a dir '.svn' in ds """
if ".svn" in ds:
ds.remove(".svn")
return os.path.basename(p) == test_dirname
def match_mod(fs):
""" test for any test modules; deletes all non-tests """
# NB: do not delete fs elements within a "for f in fs"
dels= [f for f in fs if not f.endswith(test_suffix)]
for f in dels:
fs.remove(f)
return len(fs) > 0
test_suites = []
perf_suites = []
# note that test_mod and match_mod modify passed-in lists
paths = [(path,files) for path,dirs,files in os.walk(loc) \
if test_mod(path,dirs) and match_mod(files)]
oldpath = list(sys.path)
for (dir,test_modules) in paths:
sys.path.append(dir)
for module in test_modules:
if not module.endswith(test_suffix):
raise ValueError
mod = __import__(module[:-len(".py")])
if getattr(mod,"suite",None):
test_suites.append(mod.suite())
else:
test_suites.append(ldr.loadTestsFromModule(mod))
try:
perf_suites.append(mod.perfSuite())
except:
pass
# remove temporary paths added
sys.path = list(oldpath)
return (test_suites,perf_suites)
if __name__ == '__main__':
def logging_init():
global logger
global console
console = logging.StreamHandler()
console.setLevel(logging.INFO)
console.setFormatter(logging.Formatter(
'%(name)-12s: %(levelname)-8s %(message)s'))
logger = logging.getLogger('Gramps')
logger.addHandler(console)
return console, logger
def logging_adjust(verbose_level):
if verbose_level == 1:
logger.setLevel(logging.INFO)
console.setLevel(logging.INFO)
elif verbose_level == 2:
logger.setLevel(logging.DEBUG)
console.setLevel(logging.DEBUG)
elif verbose_level == 3:
logger.setLevel(logging.NOTSET)
console.setLevel(logging.NOTSET)
elif verbose_level >= 4:
logger.setLevel(logging.NOTSET)
console.setLevel(logging.NOTSET)
os.environ['GRAMPS_SIGNAL'] = "1"
else:
logger.setLevel(logging.ERROR)
console.setLevel(logging.ERROR)
console,logger = logging_init()
options,args = make_parser().parse_args()
logging_adjust(options.verbose_level)
# TODO allow multiple subdirs, modules, or testnames
# (see unittest.py)
# hmmm this is starting to look like a unittest.Testprog
# (maybe with a custom TestLoader)
if args and os.path.isdir(args[0]):
loc = args[0]
else:
loc = gramps_root
utests, ptests = getTestSuites(loc)
if options.performance:
suite = unittest.TestSuite(ptests)
else:
suite = unittest.TestSuite(utests)
unittest.TextTestRunner(verbosity=options.verbose_level).run(suite)
#===eof===

View File

@ -0,0 +1,167 @@
"""unittest (test_util_test.py) for test_util.py"""
import sys
import os
import tempfile
import unittest as U
usage_note="""
**************************************************************
Testing (and runing) Gramps requires that PYTHONPATH include
the path to the top Gramps directory (where gramps.py resides).
For example, in bash, a shell export would look like
export PYTHONPATH=/.../src
with the ... filled in appropriately.
**************************************************************
"""
# **************************************************************
#
# Since this module is used by other test modules, it is
# strongly advised to test this module to 100% coverage,
# and in all calling variations, eg:
# run directly, from this dir with and without ./ prefix
# run from other dirs (with path prefix)
# run from within regrtest.py
# run from regrtest.py with other test modules present
# which use the test_util module itself
#
# **************************************************************
try:
from test import test_util as tu
##here = tu.absdir()
except ImportError:
print "Cannot import 'test_util'from package 'test'" + usage_note
exit(1)
# grouping into multiple TestCases (classes) is not required,
# but may be useful for various reasons, such as collecting
# tests that share a setUp/tearDown mechanism or that share
# some test data, or just because they're related.
#
# The test function name should not have docstrings, but should
# have names which add to the value of failure reporting, and
# which make it easy to find them within the source.
# some enabling infrastructure features
class Test1(U.TestCase):
def test1a_custom_exception(s):
tmsg = "testing"
try:
err = None
raise tu.TestError(tmsg)
except tu.TestError,e:
emsg = e.value
s.assertEqual(emsg, tmsg,
"raising TestError: g=%r e=%r" % (emsg, tmsg))
def test1b_msg_reporting_utility(s):
g,e = "got this", "expected that"
m,p = "your message here", "pfx"
tmsg0 = m + "\n .....got:'" + g + \
"'\n expected:'" + e +"'"
tmsg1 = p + ": " + tmsg0
s.assertEqual(tu.msg(g,e,m), tmsg0, "non-prefix message")
s.assertEqual(tu.msg(g,e,m,p), tmsg1, "prefix message")
# path-related features (note use of tu.msg tested above)
class Test2(U.TestCase):
def test2a_context_via_traceback(s):
e = __file__.rstrip(".co") # eg in *.py[co]
g = tu._caller_context()[0]
g.rstrip('c')
s.assertEqual(g,e, tu.msg(g,e, "_caller_context"))
def test2b_absdir(s):
here = tu.absdir();
g=tu.absdir(__file__)
s.assertEqual(g,here, tu.msg(g,here, "absdir"))
def test2c_path_append_parent(s):
here = tu.absdir();
par = os.path.dirname(here)
was_there = par in sys.path
if was_there:
while par in sys.path:
sys.path.remove(par)
np = len(sys.path)
for p in (None, __file__):
s.assertFalse(par in sys.path, "par not in initial path")
if not p:
g = tu.path_append_parent()
else:
g = tu.path_append_parent(p)
s.assertEqual(g,par, tu.msg(g,par, "path_append_parent return"))
s.assertTrue(par in sys.path, "actually appends")
sys.path.remove(par)
l= len(sys.path)
s.assertEqual(l,np, tu.msg(l,np,"numpaths"))
if was_there:
# restore entry state (but no multiples needed!)
sys.path.append(par)
# make and remove test dirs
class Test3(U.TestCase):
here = tu.absdir()
bases = (here, tempfile.gettempdir())
asubs = map(lambda b: os.path.join(b,"test_sub"), bases)
home= os.environ["HOME"]
if home:
home_junk = os.path.join(home,"test_junk")
def _rmsubs(s):
import shutil
for sub in s.asubs:
if os.path.isdir(sub):
shutil.rmtree(sub)
def setUp(s):
s._rmsubs()
if s.home and not os.path.isdir(s.home_junk):
os.mkdir(s.home_junk)
def tearDown(s):
s._rmsubs()
if s.home and os.path.isdir(s.home_junk):
os.rmdir(s.home_junk)
def test3a_subdir(s):
for sub in s.asubs:
s.assertFalse(os.path.isdir(sub), "init: no dir %r" % sub)
b,d = os.path.dirname(sub), os.path.basename(sub)
md = tu.make_subdir(d, b)
s.assertTrue(os.path.isdir(sub), "made dir %r" % sub)
s.assertEqual(md,sub, tu.msg(md,sub,
"make_subdir returns path"))
s2 = os.path.join(sub,"sub2")
tu.make_subdir("sub2", sub)
s.assertTrue(os.path.isdir(s2), "made dir %r" % s2)
f = os.path.join(s2,"test_file")
open(f,"w").write("testing..")
s.assertTrue(os.path.isfile(f), "file %r exists" % f)
tu.delete_tree(sub)
s.assertFalse(os.path.isdir(sub),
"delete_tree removes subdir %r" % sub )
def test3b_delete_tree_constraint(s):
if s.home:
err = None
try:
tu.delete_tree(s.home_junk)
except tu.TestError, e:
err = e.value
s.assertFalse(err is None,
"deltree on %r raises TestError" % (s.home_junk))
else:
s.fail("Skip deltree constraint test, no '$HOME' var")
if __name__ == "__main__":
U.main()
#===eof===

143
src/test/test_util.py Normal file
View File

@ -0,0 +1,143 @@
"""unittest support utility module"""
import os
import sys
import traceback
import tempfile
import shutil
# _caller_context is primarily here to support and document the process
# of determining the test-module's directory.
#
# NB: the traceback 0-element is 'limit'-levels back, or earliest calling
# context if that is less than limit.
# The -1 element is this very function; -2 is its caller, etc.
# A traceback context tuple is:
# (file, line, active function, text of the call-line)
def _caller_context():
"""Return context of first caller outside this module"""
lim = 5 # 1 for this function, plus futrher chain within this module
st = traceback.extract_stack(limit=lim)
thisfile = __file__.rstrip("co") # eg, in ".py[co]
while st and st[-1][0] == thisfile:
del(st[-1])
if not st:
raise TestError("Unexpected function call chain length!")
return st[-1]
# NB: tb[0] differs between running 'XYZ_test.py' and './XYZ_test.py'
# so, always take the abspath.
def _caller_dir():
"""Return directory of caller function (caller outside this module)"""
tb = _caller_context()
return os.path.dirname(os.path.abspath(tb[0]))
class TestError(Exception):
"""Exception for use by test modules
Use this, for example, to distuinguish testing errors.
"""
def __init__(self, value):
self.value = value
def __str__(self):
return repr(self.value)
def msg(got, exp, msg, pfx=""):
"""Error-report message formatting utility
This improves unittest failure messages by showing data values
Usage:
assertEqual(got,exp, msg(got,exp,"mess" [,prefix])
The failure message will show as
[prefix: ] mess
.....got:repr(value-of-got)
expected:repr(value-of-exp)
"""
if pfx:
pfx += ": "
return "%s%s\n .....got:%r\n expected:%r" % (pfx, msg, got, exp)
def absdir(path=None):
"""Return absolute dir of the specified path
The path parm may be dir or file or missing.
If a file, the dir of the file is used.
If missing, the dir of test-module caller is used
Common usage is
here = absdir()
here = absdir(__file__)
These 2 return the same result
"""
if not path:
path = _caller_dir()
loc = os.path.abspath(path)
if os.path.isfile(loc):
loc = os.path.dirname(loc)
return loc
def path_append_parent(path=None):
"""Append (if required) the parent of a path to the python system path,
and return the abspath to the parent as a possible convenience
The path parm may be a dir or a file or missing.
If a file, the the dir of the file is used.
If missing the test-module caller's dir is used.
And then the parent of that dir is appended (if not already present)
Common usage is
path_append_parent()
path_append_parent(__file__)
These 2 produce the same result
"""
pdir = os.path.dirname(absdir(path))
if not pdir in sys.path:
sys.path.append(pdir)
return pdir
def make_subdir(dir, parent=None):
"""Make (if required) a subdir to a given parent and return its path
The parent parm may be dir or file or missing
If a file, the dir of the file us used
If missing, the test-module caller's dir is used
Then the subdir dir in the parent dir is created if not already present
"""
if not parent:
parent = _caller_dir()
sdir = os.path.join(parent,dir)
if not os.path.exists(sdir):
os.mkdir(sdir)
return sdir
def delete_tree(dir):
"""Recursively delete directory and content
WARNING: this is clearly dangerous
it will only operate on subdirs of the test module dir or of /tmp
Test writers may explicitly use shutil.rmtree if really needed
"""
if not os.path.isdir(dir):
raise TestError("%r is not a dir" % dir)
sdir = os.path.abspath(dir)
here = _caller_dir() + os.path.sep
tmp = tempfile.gettempdir() + os.path.sep
if not (sdir.startswith(here) or sdir.startswith(tmp)):
raise TestError("%r is not a subdir of here (%r) or %r"
% (dir, here, tmp))
shutil.rmtree(sdir)
#===eof===