e584704e7b
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.
275 lines
8.7 KiB
Python
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===
|