gramps/gramps/test/test_util.py
Paul Culley e584704e7b Update Merge tests to actually run
So this was a lot of work...

Updated to use lxml, steep learning curve for someone who never examined XML before...

The merge test code needed some updates because it was last used with an older version of the Gramps XML.

Found another bug in the mergefamilyquery code when test started running; another nonetype issue.

Found another bug in the mergepersonquery code when test started running; another nonetype issue.

Couldn't get the subprocess stuff to work right, so changed code to just call Gramps with capture, similar to export_tests. This in turn required that importxml and exportxml be slightly changed to support StringIO redirection of stdin and stdout.

And test_util needed a change to allow stdout to accept an unencoded stream, so I added an option to use BytesIO for this test as well.

Added some code to save input, result, and expected data to files in Gramps temp directory for debugging whenever an error occurred. Easier to use my editor in diff mode than look at the outputs.
2016-12-06 11:17:54 +11:00

275 lines
8.7 KiB
Python

#
# Gramps - a GTK+/GNOME based genealogy program
#
# Copyright (C) 2000-2007 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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
#
# test/test_util.py
"""unittest support utility module"""
import os
import sys
import traceback
import tempfile
import shutil
import logging
import contextlib
from io import TextIOWrapper, BytesIO, StringIO
from gramps.gen.dbstate import DbState
from gramps.cli.user import User
from gramps.cli.grampscli import CLIManager
from gramps.cli.argparser import ArgParser
from gramps.cli.arghandler import ArgHandler
# _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 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)
# simplified logging
# gramps-independent but gramps-compatible
#
# I don't see any need to inherit from logging.Logger
# (at present, test code needs nothing fancy)
# but that might be considered for future needs
# NB: current code reflects limited expertise on the
# uses of the logging module
# ---------------------------------------------------------
class TestLogger:
"""this class mainly just encapsulates some globals
namely lfname, lfh for a file log name and handle
provides simplified logging setup for test modules
that need to setup logging for modules under test
(just instantiate a TestLogger to avoid error
messages about logging handlers not available)
There is also a simple logfile capability, to allow
test modules to capture gramps logging output
Note that existing logging will still occur, possibly
resulting in console messages and popup dialogs
"""
def __init__(self, lvl=logging.WARN):
logging.basicConfig(level=lvl)
def logfile_init(self, lfname):
"""init or re-init a logfile"""
if getattr(self, "lfh", None):
logging.getLogger().handlers.remove(self.lfh)
if os.path.isfile(lfname):
os.unlink(lfname)
self.lfh = logging.FileHandler(lfname)
logging.getLogger().addHandler(self.lfh)
self.lfname = lfname
def logfile_getlines(self):
"""get current content of logfile as list of lines"""
txt = []
if self.lfname and os.path.isfile(self.lfname):
txt = open(self.lfname).readlines()
return txt
### Support for testing CLI
def new_exit(edit_code=None):
raise SystemExit()
@contextlib.contextmanager
def capture(stdin, bytesio=False):
oldout, olderr = sys.stdout, sys.stderr
oldexit = sys.exit
if stdin:
oldin = sys.stdin
sys.stdin = stdin
try:
output = [BytesIO() if bytesio else StringIO(), StringIO()]
sys.stdout, sys.stderr = output
sys.exit = new_exit
yield output
except SystemExit:
pass
finally:
sys.stdout, sys.stderr = oldout, olderr
sys.exit = oldexit
if stdin:
sys.stdin = oldin
output[0] = output[0].getvalue()
output[1] = output[1].getvalue()
class Gramps:
def __init__(self, user=None, dbstate=None):
## Setup:
from gramps.cli.clidbman import CLIDbManager
self.dbstate = dbstate or DbState()
#we need a manager for the CLI session
self.user = user or User(auto_accept=True, quiet=False)
self.climanager = CLIManager(self.dbstate, setloader=True, user=self.user)
self.clidbmanager = CLIDbManager(self.dbstate)
def run(self, *args, stdin=None, bytesio=False):
with capture(stdin, bytesio=bytesio) as output:
#load the plugins
self.climanager.do_reg_plugins(self.dbstate, uistate=None)
# handle the arguments
args = [sys.executable] + list(args)
argparser = ArgParser(args)
argparser.need_gui() # initializes some variables
if argparser.errors:
print(argparser.errors, file=sys.stderr)
argparser.print_help()
argparser.print_usage()
handler = ArgHandler(self.dbstate, argparser, self.climanager)
# create a manager to manage the database
handler.handle_args_cli()
if handler.dbstate.is_open():
handler.dbstate.db.close()
return output
#===eof===